You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kylin.apache.org by li...@apache.org on 2024/09/02 08:19:47 UTC

(kylin) branch kylin5 updated: KYLIN-5960 Add front-end for recommendation

This is an automated email from the ASF dual-hosted git repository.

liyang pushed a commit to branch kylin5
in repository https://gitbox.apache.org/repos/asf/kylin.git


The following commit(s) were added to refs/heads/kylin5 by this push:
     new 6f5c8cd42e KYLIN-5960 Add front-end for recommendation
6f5c8cd42e is described below

commit 6f5c8cd42eaee9476eb84e4eba25a693ba35c902
Author: Pengfei Zhan <de...@gmail.com>
AuthorDate: Fri Aug 30 19:01:19 2024 +0800

    KYLIN-5960 Add front-end for recommendation
---
 .../components/common/UploadSql/SuggestModel.vue   |  495 +++++++
 .../src/components/common/UploadSql/UploadSql.vue  | 1369 ++++++++++++++++++++
 .../src/components/common/UploadSql/locales.js     |  156 +++
 kystudio/src/components/common/UploadSql/store.js  |   50 +
 .../setting/SettingBasic/SettingBasic.vue          |  279 +++-
 .../src/components/setting/SettingBasic/locales.js |    2 +-
 .../ModelList/ModelActions/modelActions.vue        |    3 +
 .../ModelAggregate/sub/recommendations.vue         | 1082 ++++++++++++++++
 .../ModelList/ModelLayout/modelLayout.vue          |   30 +-
 .../studio/StudioModel/ModelList/index.vue         |  129 +-
 kystudio/src/config/index.js                       |    1 +
 kystudio/src/config/spec.js                        |    7 +-
 kystudio/src/directive/index.js                    |   42 +
 kystudio/src/locale/en.js                          |    1 +
 kystudio/src/service/model.js                      |   44 +
 kystudio/src/service/project.js                    |    8 +
 kystudio/src/store/model.js                        |   29 +-
 kystudio/src/store/project.js                      |   19 +-
 kystudio/src/store/system.js                       |    2 +
 kystudio/src/store/types.js                        |   11 +
 kystudio/src/util/business.js                      |   14 +-
 21 files changed, 3746 insertions(+), 27 deletions(-)

diff --git a/kystudio/src/components/common/UploadSql/SuggestModel.vue b/kystudio/src/components/common/UploadSql/SuggestModel.vue
new file mode 100644
index 0000000000..eb2136a898
--- /dev/null
+++ b/kystudio/src/components/common/UploadSql/SuggestModel.vue
@@ -0,0 +1,495 @@
+<template>
+  <div class="model-layout">
+    <el-row :gutter="15" v-if="!isOriginModelsTable">
+      <el-col :span="15">
+        <el-table
+          :data="suggestModels"
+          class="model-table"
+          v-scroll-shadow
+          :ref="tableRef"
+          style="width: 100%"
+          @sort-change="onSortChangeModels"
+          @select="handleSelectionModel"
+          @selection-change="handleSelectionModelChange"
+          @select-all="handleSelectionAllModel"
+          @row-click="modelRowClick"
+          :row-class-name="setRowClass"
+        >
+          <el-table-column type="selection" width="44"></el-table-column>
+          <el-table-column :label="$t('kylinLang.model.modelNameGrid')">
+            <template slot-scope="scope">
+              <el-input v-model="scope.row.alias" :class="{'name-error': scope.row.isNameError}" size="small" @change="handleRename(scope.row)"></el-input>
+              <div class="rename-error" v-if="scope.row.isNameError">{{modelNameError}}</div>
+            </template>
+          </el-table-column>
+          <el-table-column :label="$t('kylinLang.common.fact')" prop="fact_table" show-overflow-tooltip width="140"></el-table-column>
+          <el-table-column label="SQL" prop="sqls" width="100" align="right" sortable="custom" :render-header="renderHeaderSql">
+            <template slot-scope="scope">{{scope.row.rec_items[0].sqls.length || 0}}</template>
+          </el-table-column>
+        </el-table>
+      </el-col>
+      <el-col class="details" :span="9">
+        <el-table
+          v-if="activeRowId"
+          :data="modelDetails"
+          class="model-table"
+          v-scroll-shadow
+          ref="newModelDetails"
+          style="width: 100%">
+          <el-table-column type="expand" width="44">
+            <template slot-scope="scope">
+              <template v-if="scope.row.type === 'cc'">
+                <p><span class="label">{{$t('th_expression')}}:</span>{{scope.row.cc.expression}}</p>
+              </template>
+              <template v-if="scope.row.type === 'dimension'">
+                <p><span class="label">{{$t('th_column')}}:</span>{{scope.row.dimension.name}}</p>
+              </template>
+              <template v-if="scope.row.type === 'measure'">
+                <p><span class="label">{{$t('th_column')}}:</span>{{scope.row.measure.name}}</p>
+                <p><span class="label">{{$t('th_function')}}:</span>{{scope.row.measure.function.expression}}</p>
+                <p><span class="label">{{$t('th_parameter')}}:</span>{{scope.row.measure.function.parameters}}</p>
+              </template>
+            </template>
+          </el-table-column>
+          <el-table-column :label="$t('th_name')" show-overflow-tooltip>
+            <template slot-scope="scope">
+              {{scope.row.name || scope.row.columnName}}
+            </template>
+          </el-table-column>
+          <el-table-column :label="$t('th_type')" prop="type" width="80">
+            <template slot-scope="scope">
+              {{ $t(scope.row.type) }}
+            </template>
+          </el-table-column>
+        </el-table>
+        <div class="no-model-data" v-else>
+          <span class="icon el-icon-ksd-select"></span>
+          <p>{{$t('noModelDetailsTip')}}</p>
+        </div>
+      </el-col>
+    </el-row>
+    <el-row :gutter="15" v-else>
+      <div class="recommend-layout" v-show="hasRecommendation">
+        <el-col :span="15">
+          <el-table
+            :data="recommendDatas"
+            class="model-table"
+            border
+            v-scroll-shadow
+            :ref="tableRef"
+            @sort-change="onSortChange"
+            @select="handleSelectionRecommends"
+            @selection-change="handleSelectionRecommendationChange"
+            @row-click="recommendRowClick"
+            :row-class-name="setRowClass">
+            <el-table-column type="selection" width="44"></el-table-column>
+            <el-table-column :label="$t('kylinLang.model.modelNameGrid')" show-overflow-tooltip>
+              <template slot-scope="scope">
+                <span>{{scope.row.alias}}</span>
+              </template>
+            </el-table-column>
+            <el-table-column :label="$t('kylinLang.model.index')" show-overflow-tooltip prop="index_id">
+            </el-table-column>
+            <el-table-column label="SQL" prop="sqls" width="100" align="right" sortable="custom" :render-header="renderHeaderSql">
+              <template slot-scope="scope">{{scope.row.sqls.length || 0}}</template>
+            </el-table-column>
+          </el-table>
+        </el-col>
+        <el-col class="details" :span="9">
+          <el-table
+            v-if="activeRowId"
+            :data="recommendDetails"
+            class="model-table"
+            border
+            ref="originModelDetails"
+            v-scroll-shadow
+            style="width: 100%">
+            <el-table-column type="expand" width="44">
+              <template slot-scope="scope">
+                <template v-if="scope.row.type === 'cc'">
+                  <p><span class="label">{{$t('th_expression')}}:</span>{{scope.row.cc.expression}}</p>
+                </template>
+                <template v-if="scope.row.type === 'dimension'">
+                  <p><span class="label">{{$t('th_column')}}:</span>{{scope.row.name}}</p>
+                </template>
+                <template v-if="scope.row.type === 'measure'">
+                  <p><span class="label">{{$t('th_column')}}:</span><span class="break-word">{{scope.row.measure.name}}</span></p>
+                  <p><span class="label">{{$t('th_function')}}:</span>{{scope.row.measure.function.expression}}</p>
+                  <p><span class="label">{{$t('th_parameter')}}:</span>{{scope.row.measure.function.parameters}}</p>
+                </template>
+              </template>
+            </el-table-column>
+            <el-table-column :label="$t('th_name')" prop="name" show-overflow-tooltip>
+            </el-table-column>
+            <el-table-column :label="$t('th_type')" prop="type" width="80">
+              <template slot-scope="scope">
+                {{ $t(scope.row.type) }}
+              </template>
+            </el-table-column>
+          </el-table>
+          <div class="no-origin-data" v-else>
+            <span class="icon el-icon-ksd-select"></span>
+            <p>{{$t('noOriginDetailsTip')}}</p>
+          </div>
+        </el-col>
+      </div>
+      <template v-if="!hasRecommendation">
+        <div class="no-recommends">
+          <span class="icon el-icon-ksd-empty-box"></span>
+          <p>{{$t('noRecommendsTip')}}</p>
+        </div>
+      </template>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue'
+import { Component } from 'vue-property-decorator'
+import { NamedRegex } from 'config'
+import { handleSuccess } from '../../../util/business'
+import { handleError, ArrayFlat } from '../../../util/index'
+import { mapActions, mapGetters } from 'vuex'
+import locales from './locales'
+@Component({
+  props: ['suggestModels', 'tableRef', 'isOriginModelsTable', 'maxHeight'],
+  computed: {
+    ...mapGetters([
+      'currentSelectedProject'
+    ])
+  },
+  methods: {
+    ...mapActions({
+      validateModelName: 'VALIDATE_MODEL_NAME'
+    })
+  },
+  locales
+})
+export default class SuggestModel extends Vue {
+  ArrayFlat = ArrayFlat
+  modelNameError = ''
+  isNameErrorModelExisted = ''
+  selectModels = []
+  modelDetails = []
+  selectRecommends = []
+  recommendDetails = []
+  activeRowId = ''
+  recommendDatas = []
+  mounted () {
+    this.recommendDatas = [...this.ArrayFlat(this.suggestModels.map(item => item.rec_items.map(it => ({ ...it, alias: item.alias }))))]
+    this.$nextTick(() => {
+      if (this.isOriginModelsTable) {
+        this.recommendDatas.forEach(it => {
+          this.$refs[this.tableRef] && this.$refs[this.tableRef].toggleRowSelection(it)
+        })
+      } else {
+        this.suggestModels.forEach((model) => {
+          this.$refs[this.tableRef] && this.$refs[this.tableRef].toggleRowSelection(model)
+        })
+      }
+      this.initClickItem()
+      document.addEventListener('click', this.handleClickEvent)
+      // 解决table初次渲染有大部分空白问题
+      this.$refs[this.tableRef] && this.$refs[this.tableRef].doLayout()
+    })
+  }
+
+  get modelTips () {
+    if (this.isOriginModelsTable) {
+      return this.$t('originModelTips')
+    } else {
+      return this.$t('newModelTips')
+    }
+  }
+
+  get hasRecommendation () {
+    let flag = false
+    const layouts = [...this.ArrayFlat(this.suggestModels.map(it => it.rec_items))]
+    for (let i = 0; i <= layouts.length - 1; i++) {
+      const { computed_columns, dimensions, measures } = layouts[i]
+      if (computed_columns.length + dimensions.length + measures.length > 0) {
+        flag = true
+        break
+      }
+    }
+    return flag
+  }
+
+  onSortChangeModels ({ column, prop, order }) {
+    if (prop !== 'sqls') return
+    if (order === 'ascending') {
+      this.suggestModels.sort((a, b) => {
+        return a.rec_items[0].sqls.length - b.rec_items[0].sqls.length
+      })
+    } else {
+      this.suggestModels.sort((a, b) => {
+        return b.rec_items[0].sqls.length - a.rec_items[0].sqls.length
+      })
+    }
+  }
+
+  onSortChange ({ column, prop, order }) {
+    if (prop !== 'sqls') return
+    if (order === 'ascending') {
+      this.recommendDatas.sort((a, b) => {
+        return a.sqls.length - b.sqls.length
+      })
+    } else {
+      this.recommendDatas.sort((a, b) => {
+        return b.sqls.length - a.sqls.length
+      })
+    }
+  }
+
+  initClickItem () {
+    if (this.isOriginModelsTable) {
+      this.recommendDatas.length && this.recommendRowClick(this.recommendDatas[0])
+    } else {
+      this.suggestModels.length && this.modelRowClick(this.suggestModels[0])
+    }
+  }
+
+  handleClickEvent (e) {
+    if (!e.target.closest('.row-click') && !e.target.closest('.active-row') && !e.target.closest('.details')) {
+      this.activeRowId = ''
+    }
+  }
+
+  renderHeaderSql (h, { column, index }) {
+    return <span class="sql-header">
+      SQL
+      <el-tooltip content={ this.$t('sqlInfo') } effect="dark" placement="top">
+        <span class="icon el-icon-ksd-what ksd-ml-5"></span>
+      </el-tooltip>
+    </span>
+  }
+
+  setRowClass ({ row }) {
+    return row.uuid === this.activeRowId ? 'active-row' : 'row-click'
+  }
+
+  modelRowClick (row) {
+    const computed_columns = this.ArrayFlat(row.rec_items.map(it => it.computed_columns))
+    const dimensions = this.ArrayFlat(row.rec_items.map(it => it.dimensions))
+    const measures = this.ArrayFlat(row.rec_items.map(it => it.measures))
+    this.activeRowId = row.uuid
+    this.modelDetails = [...measures.map(it => ({ ...it, name: it.measure.name, type: 'measure' })), ...dimensions.map(it => ({ ...it, name: it.dimension.name, type: 'dimension' })), ...computed_columns.map(it => ({ ...it, name: it.cc.columnName, type: 'cc' }))]
+    this.$nextTick(() => {
+      this.$refs.newModelDetails && this.$refs.newModelDetails.doLayout()
+    })
+  }
+
+  recommendRowClick (row) {
+    this.activeRowId = row.index_id
+    this.recommendDetails = [...row.computed_columns.map(it => ({ ...it, name: it.cc.columnName, type: 'cc' })), ...row.dimensions.map(it => ({ ...it.dimension, type: 'dimension' })), ...row.measures.map(it => ({ ...it, name: it.measure.name, type: 'measure' }))]
+    this.$nextTick(() => {
+      this.$refs.originModelDetails && this.$refs.originModelDetails.doLayout && this.$refs.originModelDetails.doLayout()
+    })
+  }
+
+  sqlsTable (sqls) {
+    return sqls.map((s) => {
+      return { sql: s }
+    })
+  }
+
+  handleRename (model) {
+    let suggestListRename = false
+    model.isNameError = false
+    this.modelNameError = ''
+    if (model.isChecked) {
+      if (model.alias.trim().length > 127) {
+        model.isNameError = true
+        suggestListRename = true
+        this.modelNameError = this.$t('kylinLang.common.overLength127Tip')
+        this.checkRenameModelExisted()
+      }
+      if (!NamedRegex.test(model.alias.trim())) {
+        model.isNameError = true
+        suggestListRename = true
+        this.modelNameError = this.$t('kylinLang.common.nameFormatValidTip')
+        this.checkRenameModelExisted()
+      }
+      if (!suggestListRename) {
+        for (let m = 0; m < this.suggestModels.length; m++) {
+          if (this.suggestModels[m].uuid !== model.uuid && this.suggestModels[m].alias === model.alias.trim()) {
+            model.isNameError = true
+            suggestListRename = true
+            this.modelNameError = this.$t('renameError')
+            this.checkRenameModelExisted()
+            break
+          }
+        }
+      }
+      if (!suggestListRename) {
+        this.validateModelName({ alias: model.alias.trim(), uuid: model.uuid, project: this.currentSelectedProject }).then((res) => {
+          handleSuccess(res, (data) => {
+            if (data) {
+              model.isNameError = true
+              this.modelNameError = this.$t('renameError')
+            }
+            this.checkRenameModelExisted()
+          })
+        }, (res) => {
+          handleError(res)
+        })
+      }
+    }
+  }
+
+  handleSelectionModel (selection, row) {
+    row.isChecked = !row.isChecked
+    if (!this.isOriginModelsTable) {
+      this.handleRename(row)
+    }
+  }
+
+  handleSelectionModelChange (selection) {
+    this.selectModels = selection
+    this.$emit('getSelectModels', this.selectModels)
+  }
+
+  handleSelectionAllModel (selection) {
+    if (selection.length) {
+      selection.forEach((m) => {
+        m.isChecked = true
+        if (!this.isOriginModelsTable) {
+          this.handleRename(m)
+        }
+      })
+    } else {
+      this.suggestModels.forEach((m) => {
+        m.isChecked = false
+        if (!this.isOriginModelsTable) {
+          this.handleRename(m)
+        }
+      })
+    }
+  }
+
+  handleSelectionRecommends (selection, row) {
+    row.isChecked = !row.isChecked
+  }
+
+  handleSelectionRecommendationChange (val) {
+    this.selectRecommends = val
+    const suggestList = [...JSON.parse(JSON.stringify(this.suggestModels))]
+    const layoutIds = val.map(it => it.index_id)
+    const data = [...suggestList.filter(it => {
+      return it.rec_items.some(item => layoutIds.includes(item.index_id))
+    })]
+    data.forEach(it => {
+      it.rec_items = it.rec_items.filter(item => layoutIds.includes(item.index_id))
+    })
+    this.$emit('getSelectRecommends', data)
+    this.$emit('changeSelectRecommendsLength', val.length)
+  }
+
+  checkRenameModelExisted () {
+    this.isNameErrorModelExisted = false
+    for (let i = 0; i < this.suggestModels.length; i++) {
+      if (this.suggestModels[i].isChecked && this.suggestModels[i].isNameError) {
+        this.isNameErrorModelExisted = true
+        break
+      }
+    }
+    this.$emit('isValidated', this.isNameErrorModelExisted)
+  }
+
+  beforeDestroy () {
+    document.removeEventListener('click', this.handleClickEvent)
+  }
+}
+</script>
+<style lang="less">
+  @import '../../../assets/styles/variables.less';
+  .rename-error {
+    color: @error-color-1;
+    font-size: 12px;
+    line-height: 1.2;
+  }
+  .break-word {
+    word-break: break-all;
+  }
+  .model-table {
+    .el-table__row {
+      cursor: pointer;
+    }
+    .active-row {
+      background-color: @base-color-9;
+    }
+    .el-table__expanded-cell {
+      padding: 24px;
+      font-size: 12px;
+      color: @text-title-color;
+      p {
+        margin-bottom: 5px;
+        word-break: break-all;
+        &:last-child {
+          margin-bottom: 0;
+        }
+      }
+      .label {
+        color: @text-normal-color;
+      }
+    }
+    .sql-header {
+      .icon {
+        cursor: pointer;
+      }
+    }
+  }
+  .model-layout {
+    height: 100%;
+    .el-row {
+      height: 100%;
+      .el-col {
+        height: 100%;
+        padding: 20px 0;
+        overflow: auto;
+      }
+      .el-col.details {
+        border-left: 1px solid @ke-border-divider-color;
+      }
+    }
+    .model-table {
+      .el-table__body-wrapper {
+        max-height: calc(~'100% - 40px');
+      }
+    }
+    .recommend-layout {
+      height: 100%;
+    }
+  }
+  .no-model-data, .no-origin-data {
+    text-align: center;
+    margin: 0 60px;
+    font-size: 12px;
+    position: absolute;
+    top: 50%;
+    transform: translate(0, -50%);
+    color: @text-normal-color;
+    .icon {
+      font-size: 37px;
+      color: @text-placeholder-color;
+      margin-bottom: 5px;
+    }
+  }
+  .no-recommends {
+    width: 300px;
+    font-size: 12px;
+    position: absolute;
+    margin: auto;
+    left: 0;
+    right: 0;
+    top: 150px;
+    text-align: center;
+    color: @text-normal-color;
+    .icon {
+      color: @text-placeholder-color;
+      font-size: 36px;
+      margin-bottom: 5px;
+      // border: 1px dashed @text-placeholder-color;
+    }
+  }
+</style>
diff --git a/kystudio/src/components/common/UploadSql/UploadSql.vue b/kystudio/src/components/common/UploadSql/UploadSql.vue
new file mode 100644
index 0000000000..938f0f6725
--- /dev/null
+++ b/kystudio/src/components/common/UploadSql/UploadSql.vue
@@ -0,0 +1,1369 @@
+<template>
+  <div>
+    <el-dialog
+      :visible="isShow"
+      top="5vh"
+      width="960px"
+      limited-area
+      :close-on-press-escape="false"
+      :close-on-click-modal="false"
+      @close="handleClose"
+      :class="['importSqlDialog', {'is-step3': uploadFlag==='step3'}]">
+      <span slot="title" class="ksd-title-label">{{uploadTitle}}</span>
+      <div class="upload-block" v-if="uploadFlag==='step1'">
+        <img src="../../../assets/img/license.png" alt="" v-show="!uploadItems.length">
+        <div class="ksd-mt-10 text" v-show="!uploadItems.length">{{$t('pleImport')}}</div>
+        <!-- <span class="upload-size-tip" v-show="!uploadItems.length">{{$t('uploadSizeTip')}}</span> -->
+        <el-upload
+          ref="sqlUpload"
+          class="sql-upload"
+          :headers="uploadHeader"
+          action=""
+          :on-remove="handleRemove"
+          :on-change="fileItemChange"
+          :file-list="uploadItems"
+          multiple
+          :auto-upload="false">
+          <el-button type="primary" size="medium">{{$t('sqlFiles')}}
+          </el-button>
+          <div :class="['upload-rules', {'check-rules': showUploadRules}]">
+            <ul class="rule-list">
+              <li :class="['rule-item', item.status === 'success' ? 'is-success' : item.status === 'error' ? 'is-error' : null]" v-for="item in uploadRules" :key="item.type">
+                <span v-if="item.status === 'default'">•</span><i class="el-icon-ksd-accept" v-else-if="item.status === 'success'"></i><i v-else class="el-icon-ksd-close"></i>
+                {{item.query ? $t(item.text, item.query()): $t(item.text)}}
+              </li>
+            </ul>
+            <p class="rule-info"><span class="icon el-icon-ksd-info ksd-mr-5"></span><span class="tip-text">{{$t('uploadSizeTip')}}</span></p>
+          </div>
+        </el-upload>
+      </div>
+      <el-row :gutter="15" v-if="uploadFlag==='step2'">
+        <el-col :span="16">
+          <div class="clearfix ksd-mb-10">
+            <div class="ksd-fleft">
+              <div v-if="pagerTableData.length&&whiteSqlData.capable_sql_num" class="ksd-fleft ksd-mr-10">
+                <el-button type="primary" size="medium" plain @click="selectAll" v-if="selectSqls.length!==whiteSqlData.capable_sql_num">{{$t('checkAll')}}</el-button><el-button
+                type="primary" size="medium" plain @click="cancelSelectAll" v-else>{{$t('cancelAll')}}</el-button>
+              </div>
+
+            </div>
+            <div class="ksd-fright ksd-inline searchInput" v-if="whiteSqlData.size">
+              <el-input v-model="whiteSqlFilter" @input="onWhiteSqlFilterChange" prefix-icon="el-icon-search" :placeholder="$t('kylinLang.common.search')" size="medium"></el-input>
+            </div>
+          </div>
+          <el-table
+            :data="pagerTableData"
+            ref="multipleTable"
+            :empty-text="emptyText"
+            @row-click="activeSql"
+            @select="handleSelectionChange"
+            @select-all="handleSelectAllChange"
+            :row-class-name="tableRowClassName"
+            class="import-table"
+            style="width: 100%">
+            <el-table-column type="selection" width="44" align="center" :selectable="selectable"></el-table-column>
+            <el-table-column prop="sql" label="SQL" :resizable="false">
+              <template slot-scope="props">
+                <span class="ksd-nobr-text">{{props.row.sql}}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="capable" :label="$t('kylinLang.common.status')" width="80">
+              <template slot-scope="props">
+                <i :class="{'el-icon-ksd-good_health': props.row.capable, 'el-icon-ksd-error_01': !props.row.capable}"></i>
+              </template>
+            </el-table-column>
+            <el-table-column :label="$t('kylinLang.common.action')" width="80">
+              <template slot-scope="props">
+                <common-tip :content="$t('kylinLang.common.edit')">
+                  <i class="el-icon-ksd-table_edit" :class="{'is-disabled': activeSqlObj && props.row.id === activeSqlObj.id && isEditSql}" @click.stop="editWhiteSql(props.row)"></i>
+                </common-tip>
+                <common-tip :content="$t('kylinLang.common.drop')">
+                  <i class="el-icon-ksd-table_delete ksd-ml-10" @click.stop="delWhiteComfirm(props.row.id)"></i>
+                </common-tip>
+                </template>
+            </el-table-column>
+          </el-table>
+          <kylin-pager ref="sqlListsPager" :refTag="pageRefTags.sqlListsPager" class="ksd-center ksd-mt-10" :totalSize="filteredDataSize" :curPage="whiteCurrentPage+1" layout="total, prev, pager, next, jumper" v-on:handleCurrentChange='whiteSqlDatasPageChange' :perPageSize="whitePageSize" v-if="filteredDataSize > 0"></kylin-pager>
+        </el-col>
+        <el-col :span="8">
+          <div class="ky-list-title ksd-mt-10 ksd-fs-14">{{$t('sqlBox')}}</div>
+          <div element-loading-spinner="el-icon-loading">
+            <div v-loading="sqlLoading" class="query_panel_box ksd-mt-10">
+              <kylin-editor ref="whiteInputBox" :height="inputHeight" :dragable="false" :readOnly="this.isReadOnly" lang="sql" theme="chrome" v-model="whiteSql" v-if="isShowEditor">
+              </kylin-editor>
+              <div class="operatorBox" v-if="isEditSql">
+                <div class="btn-group ksd-fright ky-no-br-space">
+                  <el-button size="small" plain @click="cancelEdit(isWhiteErrorMessage)">{{$t('kylinLang.common.cancel')}}</el-button>
+                  <el-button size="small" :loading="validateLoading" @click="validateWhiteSql">{{$t('kylinLang.common.save')}}</el-button>
+                </div>
+              </div>
+            </div>
+            <div class="error_messages" v-if="isWhiteErrorMessage">
+              <div v-for="(mes, index) in whiteMessages" :key="index">
+                <div class="label">{{$t('messages')}}</div>
+                <p>{{mes.incapable_reason}}</p>
+                <div class="label ksd-mt-10">{{$t('suggestion')}}</div>
+                <p>{{mes.suggestion}}</p>
+              </div>
+            </div>
+          </div>
+        </el-col>
+      </el-row>
+      <div :class="['review-sql-model', {'new-model': !isShowTabModels && isShowSuggestModels, 'origin-model': !isShowTabModels && isShowOriginModels}]" v-if="uploadFlag==='step3'">
+        <el-alert class="recommendation-alert" :title="$t('recommendationTip')" type="warning" :closable="false" show-icon v-if="isShowOriginModels || isShowTabModels"></el-alert>
+        <div class="ky-list-title" v-if="isShowSuggestModels">
+          {{$t('newModelList')}} ({{selectModels.length}}/{{suggestModels.length}})
+        </div>
+        <el-alert v-if="isShowSuggestModels" :title="$t('renameTips')" show-icon :closable="false" type="tip"></el-alert>
+        <div class="ky-list-title" v-if="isShowOriginModels">
+          {{$t('recommendations')}} ({{selectRecommendsLength}}/{{getRecommendLength}})
+        </div>
+        <SuggestModel
+          v-if="isShowSuggestModels"
+          tableRef="modelsTable"
+          :suggestModels="suggestModels"
+          :maxHeight="430"
+          @isValidated="isValidated"
+          @getSelectModels="getSelectModels" />
+        <SuggestModel
+          v-if="isShowOriginModels"
+          tableRef="originModelsTable"
+          :suggestModels="originModels"
+          :maxHeight="365"
+          @getSelectRecommends="getSelectRecommends"
+          @changeSelectRecommendsLength="changeSelectRecommendsLength"
+          :isOriginModelsTable="true" />
+        <el-tabs class="upload-tabs" v-model="modelType" v-if="isShowTabModels" @tab-click="changeTabs">
+          <el-tab-pane :label="$t('model') + ` (${selectModels.length}/${suggestModels.length})`" name="suggest">
+            <SuggestModel
+              tableRef="modelsTable"
+              ref="suggestModelRef"
+              :suggestModels="suggestModels"
+              :maxHeight="365"
+              @isValidated="isValidated"
+              @getSelectModels="getSelectModels" />
+          </el-tab-pane>
+          <el-tab-pane :label="`${$t('recommendations')} (${selectRecommendsLength}/${getRecommendLength})`" name="origin">
+            <SuggestModel
+              tableRef="originModelsTable"
+              ref="originModelRef"
+              :suggestModels="originModels"
+              :maxHeight="365"
+              @getSelectRecommends="getSelectRecommends"
+              @changeSelectRecommendsLength="changeSelectRecommendsLength"
+              :isOriginModelsTable="true" />
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+      <span slot="footer" class="dialog-footer">
+        <div class="ksd-fleft" style="display: flex;" v-if="uploadFlag==='step3' && (isShowSuggestModels || isShowTabModels && modelType === 'suggest')">
+          <el-checkbox v-model="addBaseIndex">
+            <span>{{$t('addBaseIndexCheckBox')}}</span>
+          </el-checkbox>
+          <el-tooltip effect="dark" :content="$t('baseIndexTips')" placement="top">
+            <i class="el-ksd-icon-more_info_22 ksd-fs-22"></i>
+          </el-tooltip>
+        </div>
+        <div class="ksd-fleft query-count">
+          <span v-if="uploadFlag==='step2'">
+            <span><i class="el-icon-ksd-good_health"></i>{{whiteSqlData.capable_sql_num}}</span><span class="ksd-ml-10">
+            <i class="el-icon-ksd-error_01"></i>{{whiteSqlData.size-whiteSqlData.capable_sql_num}}</span>
+            <span class="merge-sql-tip"><span class="divide">|</span><i class="el-icon-ksd-alert"></i>{{$t('mergeSqlTip')}}</span>
+          </span>
+          <!-- <span class="selected-item" v-if="uploadFlag==='step3'"><i class="el-icon-ksd-alert"></i>{{isShowTabModels ? $t('selectModelsAndRecommends', {models: selectModels.length, recommends: selectRecommendsLength}) : isShowSuggestModels ? $t('selectModelTips', {models: selectModels.length}) : $t('selectRecommendTips', {recommends: selectRecommendsLength})}}</span> -->
+          <!-- <span v-if="uploadFlag==='step1'" class="tips">
+            <i class="el-icon-ksd-info ksd-fs-14"></i><span class="ksd-fs-12">{{$t('uploadFileTips')}}</span>
+          </span> -->
+        </div>
+        <div class="ky-no-br-space">
+          <el-button size="medium" @click="handleCancel" v-if="!isShowOriginModels">{{$t('kylinLang.common.cancel')}}</el-button>
+          <el-button type="primary" size="medium" v-if="uploadFlag==='step1'" :loading="importLoading" :disabled="!uploadItems.length||checkedUploadRules"  @click="submitFiles">{{$t('kylinLang.common.next')}}</el-button>
+          <el-button type="primary" size="medium" v-if="uploadFlag==='step2'&&!isGenerateModel" :disabled="!finalSelectSqls.length" :loading="submitSqlLoading" @click="submitSqls">{{$t('addTofavorite')}}</el-button>
+          <el-button type="primary" size="medium" v-if="uploadFlag==='step2'&&isGenerateModel" :loading="generateLoading" :disabled="!finalSelectSqls.length"  @click="submitSqls">{{$t('kylinLang.common.next')}}</el-button>
+          <el-button type="primary" size="medium" v-if="uploadFlag==='step3'&&isGenerateModel" :loading="submitModelLoading" :disabled="!getFinalSelectModels.length || isNameErrorModelExisted" @click="submitModels">{{$t('kylinLang.common.ok')}}</el-button>
+          <!-- <el-button type="primary" size="medium" v-if="uploadFlag==='step3'&&isGenerateModel&&isShowOriginModels" @click="handleCloseAcceptModal">{{$t('kylinLang.common.ok')}}</el-button> -->
+        </div>
+      </span>
+    </el-dialog>
+    <el-dialog
+      :visible="isConvertShow"
+      width="480px"
+      limited-area
+      :close-on-press-escape="false"
+      :close-on-click-modal="false"
+      @close="handleConvertClose"
+      class="convertDialog">
+      <span slot="title" class="ky-list-title">{{$t('kylinLang.common.tip')}}</span>
+      <div>{{$t('existedAnsweredModels')}}</div>
+      <span slot="footer" class="dialog-footer ky-no-br-space">
+        <el-button @click="convertSqlsSubmit(false)" :loading="cancelConvertLoading" size="medium">{{$t('noConvert')}}</el-button>
+        <el-button type="primary" @click="convertSqlsSubmit(true)" :loading="convertLoading" size="medium">{{$t('convert')}}</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import Vue from 'vue'
+import { Component, Watch } from 'vue-property-decorator'
+import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
+
+import vuex from '../../../store'
+import locales from './locales'
+import store, { types } from './store'
+import { handleSuccessAsync, handleError, ArrayFlat } from '../../../util/index'
+import { handleSuccess, kylinConfirm, kylinWarn } from '../../../util/business'
+import SuggestModel from './SuggestModel.vue'
+import { pageRefTags, pageCount } from '../../../config'
+
+vuex.registerModule(['modals', 'UploadSqlModel'], store)
+@Component({
+  computed: {
+    ...mapGetters([
+      'currentSelectedProject'
+    ]),
+    // Store数据注入
+    ...mapState('UploadSqlModel', {
+      isShow: state => state.isShow,
+      isGenerateModel: state => state.isGenerateModel,
+      title: state => state.title,
+      callback: state => state.callback
+    }),
+    ...mapState({
+      favoriteImportSqlMaxSize: state => +state.system.favoriteImportSqlMaxSize
+    })
+  },
+  methods: {
+    ...mapActions({
+      importSqlFiles: 'IMPORT_SQL_FILES',
+      formatSql: 'FORMAT_SQL',
+      addToFavoriteList: 'ADD_TO_FAVORITE_LIST',
+      validateWhite: 'VALIDATE_WHITE_SQL',
+      suggestModel: 'SUGGEST_MODEL',
+      saveSuggestModels: 'SAVE_SUGGEST_MODELS',
+      validateModelName: 'VALIDATE_MODEL_NAME',
+      suggestIsByAnswered: 'SUGGEST_IS_BY_ANSWERED'
+    }),
+    // Store方法注入
+    ...mapMutations('UploadSqlModel', {
+      setModal: types.SET_MODAL,
+      hideModal: types.HIDE_MODAL,
+      setModalForm: types.SET_MODAL_FORM,
+      resetModalForm: types.RESET_MODAL_FORM
+    })
+  },
+  components: {
+    SuggestModel
+  },
+  locales
+})
+export default class UploadSqlModel extends Vue {
+  pageRefTags = pageRefTags
+  ArrayFlat = ArrayFlat
+  uploadFlag = 'step1'
+  importLoading = false
+  messageInstance = null
+  uploadItems = []
+  pagerTableData = []
+  isWhiteErrorMessage = false
+  whiteMessages = []
+  validateLoading = false
+  isReadOnly = true
+  whiteSql = ''
+  isEditSql = false
+  submitSqlLoading = false
+  whiteSqlFilter = ''
+  activeSqlObj = null
+  whiteSqlData = null
+  sqlLoading = false
+  inputHeight = 479
+  selectSqls = []
+  filteredDataSize = 0
+  whiteCurrentPage = 0
+  timer = null
+  whitePageSize = pageCount
+  isShowEditor = true
+  sqlFormatterObj = {}
+  generateLoading = false
+  submitModelLoading = false
+  isNameErrorModelExisted = false
+  suggestModels = []
+  selectModels = []
+  originModels = []
+  selectRecommends = []
+  selectOriginModels = []
+  selectRecommendsLength = 0
+  modelType = 'suggest'
+  isConvertShow = false
+  convertLoading = false
+  cancelConvertLoading = false
+  convertSqls = []
+  uploadRules = {
+    fileFormat: { type: 'fileFormat', text: 'uploadRule1', status: 'default' },
+    // unValidSqls: {type: 'unValidSqls', text: 'uploadRule2', status: 'default'},
+    totalSize: { type: 'totalSize', text: 'uploadRule3', status: 'default' }
+    // sqlSizes: {type: 'sqlSizes', text: 'uploadRule4', status: 'default', query: this.queryHandler}
+  }
+
+  showUploadRules = false
+  wrongFormatFile = []
+  addBaseIndex = true
+  errMsgInstance = null
+  handleClose () {
+    this.hideModal()
+    this.resetModalForm()
+    this.resetImport()
+  }
+
+  // 点击取消按钮
+  handleCancel () {
+    if (this.uploadFlag === 'step3') {
+      this.$confirm(this.$t('closeSqlDialogTip'), this.$t('kylinLang.common.tip'), {
+        confirmButtonText: this.$t('confirmCancel'),
+        cancelButtonText: this.$t('backToEdit'),
+        centerButton: true,
+        type: 'warning'
+      }).then(() => {
+        this.handleClose()
+      }).catch(() => {
+      })
+    } else {
+      this.handleClose()
+    }
+  }
+
+  resetImport () {
+    this.uploadFlag = 'step1'
+    this.uploadItems = []
+    this.activeSqlObj = null
+    this.isEditSql = false
+    this.pagerTableData = []
+    this.whiteSqlFilter = ''
+    this.importLoading = false
+    this.sqlFormatterObj = {}
+    this.generateLoading = false
+    this.suggestModels = []
+    this.selectModels = []
+    this.selectOriginModels = []
+    this.selectRecommends = []
+    this.originModels = []
+    this.submitModelLoading = false
+    this.isNameErrorModelExisted = false
+    this.modelType = 'suggest'
+    this.selectRecommendsLength = 0
+    this.messageInstance && this.messageInstance.close()
+    this.wrongFormatFile = []
+    this.showUploadRules = false
+    for (const item in this.uploadRules) {
+      this.uploadRules[item].status = 'default'
+    }
+  }
+
+  get emptyText () {
+    return this.whiteSqlFilter ? this.$t('kylinLang.common.noResults') : this.$t('kylinLang.common.noData')
+  }
+
+  get uploadTitle () {
+    if (this.isGenerateModel) {
+      if (this.uploadFlag === 'step1') {
+        return this.title || this.$t('modelFromSql')
+      } else if (this.uploadFlag === 'step2') {
+        return this.$t('generateModel')
+      } else {
+        return this.$t('preview')
+      }
+    } else {
+      return this.$t('importSql')
+    }
+  }
+
+  get isShowSuggestModels () {
+    return this.suggestModels.length > 0 && this.originModels.length === 0
+  }
+
+  get isShowOriginModels () {
+    return this.suggestModels.length === 0 && this.originModels.length > 0
+  }
+
+  get isShowTabModels () {
+    return this.suggestModels.length > 0 && this.originModels.length > 0
+  }
+
+  get getRecommendLength () {
+    return this.ArrayFlat(this.originModels.map(it => it.rec_items)).length
+  }
+
+  @Watch('inputHeight')
+  onHeightChange (val) {
+    if (val) {
+      this.isShowEditor = false
+      this.$nextTick(() => {
+        this.isShowEditor = true
+      })
+    }
+  }
+
+  queryHandler () {
+    return { size: this.favoriteImportSqlMaxSize }
+  }
+
+  // 切换预览界面tabs
+  changeTabs (e) {
+    if (e.name === 'suggest') {
+      this.$nextTick(() => {
+        this.$refs.suggestModelRef.initClickItem()
+      })
+    } else {
+      this.$nextTick(() => {
+        this.$refs.originModelRef.initClickItem()
+      })
+    }
+  }
+
+  changeSelectRecommendsLength (len) {
+    this.selectRecommendsLength = len
+  }
+
+  tableRowClassName ({ row, rowIndex }) {
+    if (this.activeSqlObj && row.id === this.activeSqlObj.id) {
+      return 'active-row'
+    }
+    return ''
+  }
+
+  showLoading () {
+    this.sqlLoading = true
+  }
+
+  hideLoading () {
+    this.sqlLoading = false
+  }
+
+  onWhiteSqlFilterChange () {
+    clearTimeout(this.timer)
+    this.timer = setTimeout(() => {
+      this.whiteSqlDatasPageChange(0)
+    }, 500)
+  }
+
+  delWhiteComfirm (id) {
+    if (this.isEditSql) {
+      kylinWarn(this.$t('editSqlTips'))
+      return
+    }
+    kylinConfirm(this.$t('delSql'), { centerButton: true }, this.$t('delSqlTitle')).then(() => {
+      this.delWhite(id)
+    })
+  }
+
+  getSelectModels (models) {
+    this.selectModels = models
+  }
+
+  getSelectOriginModels (models) {
+    this.selectOriginModels = models
+  }
+
+  getSelectRecommends (rec) {
+    this.selectRecommends = rec
+  }
+
+  get getFinalSelectModels () {
+    return [...this.selectModels, ...this.selectRecommends]
+  }
+
+  isValidated (isNameErrorModelExisted) {
+    this.isNameErrorModelExisted = isNameErrorModelExisted
+  }
+
+  delWhite (id) {
+    for (const key in this.whiteSqlData.data) {
+      if (this.whiteSqlData.data[key].id === id) {
+        if (this.whiteSqlData.data[key].capable) {
+          this.whiteSqlData.capable_sql_num--
+        }
+        this.whiteSqlData.size--
+        this.whiteSqlData.data.splice(key, 1)
+        this.$nextTick(() => {
+          this.whiteSqlDatasPageChange(this.whiteCurrentPage)
+        })
+        break
+      }
+    }
+    for (const index in this.selectSqls) {
+      if (this.selectSqls[index].id === id) {
+        this.selectSqls.splice(index, 1)
+        break
+      }
+    }
+  }
+
+  whiteSqlDatasPageChange (currentPage, pageSize) {
+    const size = pageSize || +localStorage.getItem(this.pageRefTags.sqlListsPager) || pageCount
+    this.whiteCurrentPage = currentPage
+    this.whitePageSize = size
+    const filteredData = this.whiteFilter(this.whiteSqlData.data)
+    this.filteredDataSize = filteredData.length
+    this.pagerTableData = filteredData.slice(currentPage * size, (currentPage + 1) * size)
+    if (this.filteredDataSize) {
+      this.$nextTick(() => {
+        this.activeSql(this.pagerTableData[0])
+      })
+      const targetSelectSqls = []
+      this.pagerTableData.forEach((item) => {
+        let index = -1
+        for (const key in this.selectSqls) {
+          if (this.selectSqls[key].id === item.id) {
+            index = key
+            break
+          }
+        }
+        if (index !== -1) {
+          targetSelectSqls.push(item)
+        }
+      })
+      this.$nextTick(() => {
+        this.toggleSelection(targetSelectSqls)
+      })
+    } else {
+      this.whiteSql = ''
+      this.$refs.whiteInputBox && this.$refs.whiteInputBox.$refs.kylinEditor && this.$refs.whiteInputBox.$refs.kylinEditor.editor.setValue('')
+      this.activeSqlObj = null
+      this.isEditSql = false
+      this.whiteMessages = []
+      this.isWhiteErrorMessage = false
+      this.inputHeight = 479
+    }
+    this.$nextTick(() => {
+      this.$refs.multipleTable && this.$refs.multipleTable.doLayout()
+    })
+  }
+
+  selectAll () {
+    this.selectSqls = this.whiteSqlData.data.filter((item) => {
+      return item.capable
+    })
+    this.selectPagerSqls(true)
+  }
+
+  cancelSelectAll () {
+    this.selectSqls = []
+    this.selectPagerSqls(false)
+  }
+
+  handleSelectionChange (val, row) {
+    this.mergeSelectSqls(row)
+  }
+
+  handleSelectAllChange (val) {
+    if (val.length) {
+      val.forEach((item) => {
+        this.mergeSelectSqls(item, 'batchAdd')
+      })
+    } else {
+      this.pagerTableData.forEach((item) => {
+        this.mergeSelectSqls(item, 'batchRemove')
+      })
+    }
+  }
+
+  // 单选一条时:toggle row; batchFlag有值时:批量添加rows或者批量去除rows
+  mergeSelectSqls (row, batchFlag) {
+    let index = -1
+    for (const key in this.selectSqls) {
+      if (this.selectSqls[key].id === row.id) {
+        index = key
+        break
+      }
+    }
+    if (index === -1) {
+      if (batchFlag !== 'batchRemove') {
+        this.selectSqls.push(row)
+      }
+    } else {
+      if (batchFlag !== 'batchAdd') {
+        this.selectSqls.splice(index, 1)
+      }
+    }
+  }
+
+  handleCloseAcceptModal () {
+    this.hideModal()
+    this.resetModalForm()
+    this.resetImport()
+    this.$emit('reloadModelList')
+  }
+
+  submitModels () {
+    this.submitModelLoading = true
+    const models = [...this.selectModels.map(it => ({ ...it, with_base_index: this.addBaseIndex }))]
+    models.forEach(obj => {
+      delete obj.isChecked
+      delete obj.isNameError
+    })
+    const recommends = [...this.selectRecommends]
+    recommends.forEach(obj => {
+      delete obj.isChecked
+    })
+    const data = {
+      new_models: models,
+      reused_models: recommends
+    }
+    this.saveSuggestModels({ project: this.currentSelectedProject, ...data }).then((res) => {
+      handleSuccess(res, (data) => {
+        // this.$message.success(this.$t('kylinLang.common.actionSuccess'))
+        this.submitModelLoading = false
+        this.hideModal()
+        this.$emit('reloadModelList')
+        if (this.selectModels.length && this.selectRecommendsLength) {
+          this.$confirm(this.$t('successCreateModelsAndRecommends', { models: this.selectModels.length, recommends: this.selectRecommendsLength }), this.$t('importSuccess'), {
+            confirmButtonText: this.$t('kylinLang.common.ok'),
+            showCancelButton: false,
+            type: 'success'
+          })
+        } else if (this.selectModels.length) {
+          this.$message.success(this.$t('successCreateModels', { models: this.selectModels.length }))
+        } else {
+          this.$confirm(this.$t('successCreateRecommends', { recommends: this.selectRecommendsLength }), this.$t('importSuccess'), {
+            confirmButtonText: this.$t('kylinLang.common.ok'),
+            showCancelButton: false,
+            type: 'success'
+          })
+        }
+      })
+    }, (res) => {
+      handleError(res)
+      this.submitModelLoading = false
+      this.hideModal()
+    })
+  }
+
+  async submitSqls () {
+    const unCheckedSQL = this.whiteSqlData.capable_sql_num - this.finalSelectSqls.length
+    if (this.isEditSql) {
+      await kylinWarn(this.$t('editSqlTips'), {
+        type: 'warning',
+        closeOnClickModal: false,
+        confirmButtonText: this.$t('confirmEditSqlText')
+      })
+      try {
+        await this.validateWhiteSql()
+      } catch (e) {
+        setTimeout(() => {
+          this.$el.querySelector('.error_messages')?.scrollIntoView()
+        }, 50)
+        return
+      }
+    }
+    if (unCheckedSQL) {
+      kylinConfirm(this.$t('submitConfirm', { unCheckedSQL }), { cancelButtonText: this.$t('kylinLang.common.cancel'), confirmButtonText: this.$t('kylinLang.common.submit'), type: 'warning', centerButton: true }).then(() => {
+        this.submit()
+      })
+    } else {
+      this.submit()
+    }
+  }
+
+  submit () {
+    const sqlsData = this.finalSelectSqls
+    const sqls = sqlsData.map((item) => {
+      return item.sql
+    })
+    if (!this.isGenerateModel) {
+      this.submitSqlLoading = true
+      this.addToFavoriteList({ project: this.currentSelectedProject, sqls }).then((res) => {
+        handleSuccess(res, (data) => {
+          this.submitSqlLoading = false
+          const importedMsg = this.$t('addSuccess', { sqlsNum: sqls.length, importedNum: data.imported })
+          const existedMsg1 = data.imported < sqls.length ? this.$t('existedMsg1', { existedNum: sqls.length - data.imported }) : ''
+          const existedMsg2 = data.blacklist ? this.$t('existedMsg2', { blackNum: data.blacklist }) : ''
+          this.$alert(importedMsg + existedMsg1 + existedMsg2 + this.$t('end'), this.$t('kylinLang.common.notice'), {
+            confirmButtonText: this.$t('kylinLang.common.ok'),
+            iconClass: 'el-icon-info primary'
+          })
+          sqlsData.forEach((item) => {
+            this.delWhite(item.id)
+          })
+          this.$emit('reloadListAndSize')
+          this.hideModal()
+        })
+      }, (res) => {
+        handleError(res)
+        this.submitSqlLoading = false
+        this.hideModal()
+      })
+    } else {
+      this.generateLoading = true
+      this.suggestIsByAnswered({ project: this.currentSelectedProject, sqls }).then((res) => {
+        handleSuccess(res, (data) => {
+          if (!data) {
+            this.getSuggestModels(sqls, data)
+          } else {
+            this.isConvertShow = true
+            this.convertSqls = sqls
+          }
+        })
+      }, (res) => {
+        handleError(res)
+        this.generateLoading = false
+      })
+    }
+  }
+
+  handleConvertClose () {
+    this.isConvertShow = false
+    this.convertSqls = []
+    this.generateLoading = false
+  }
+
+  convertSqlsSubmit (reuseExistedModel) {
+    if (reuseExistedModel) {
+      this.convertLoading = true
+    } else {
+      this.cancelConvertLoading = true
+    }
+    this.getSuggestModels(this.convertSqls, reuseExistedModel)
+  }
+
+  getSuggestModels (sqls, reuseExistedModel) {
+    this.suggestModel({ project: this.currentSelectedProject, sqls, reuse_existed_model: reuseExistedModel }).then((res) => {
+      handleSuccess(res, (data) => {
+        if (!this.isShow) return
+        // 优化建议超过 1000 让其重新上传sql
+        if (data.reused_models.length > 1000) {
+          this.$confirm(this.$t('recommendsOverSizeTip'), this.$t('recommendsOverSizeTitle'), {
+            confirmButtonText: this.$t('kylinLang.common.ok'),
+            showCancelButton: false,
+            showClose: false,
+            type: 'warning'
+          }).then(() => {
+            this.generateLoading = false
+            this.convertLoading = false
+            this.cancelConvertLoading = false
+            this.isConvertShow = false
+          })
+          return
+        }
+        this.suggestModels = data.new_models.map((d) => {
+          d.isChecked = true
+          d.isNameError = false
+          return d
+        })
+        this.originModels = data.reused_models.map((d) => {
+          d.isChecked = true
+          return d
+        })
+        this.generateLoading = false
+        this.convertLoading = false
+        this.cancelConvertLoading = false
+        this.isConvertShow = false
+        this.uploadFlag = 'step3'
+      })
+    }, (res) => {
+      handleError(res)
+      this.generateLoading = false
+      this.convertLoading = false
+      this.cancelConvertLoading = false
+      this.isConvertShow = false
+    })
+  }
+
+  get finalSelectSqls () {
+    let finalSqls = []
+    finalSqls = this.selectSqls.filter((item) => {
+      return item.sql.indexOf(this.whiteSqlFilter) !== -1
+    })
+    return finalSqls
+  }
+
+  selectPagerSqls (isSelectAll) {
+    const selectedRows = isSelectAll
+      ? this.pagerTableData.filter((item) => {
+        return item.capable
+      })
+      : []
+    this.$nextTick(() => {
+      this.toggleSelection(selectedRows)
+    })
+  }
+
+  whiteFilter (data) {
+    return data.filter((sqlObj) => {
+      return sqlObj.sql.toLowerCase().indexOf(this.whiteSqlFilter.toLowerCase()) !== -1
+    })
+  }
+
+  toggleSelection (rows) {
+    if (rows && rows.length) {
+      this.$refs.multipleTable.clearSelection()
+      rows.forEach(row => {
+        this.$refs.multipleTable.toggleRowSelection(row)
+      })
+    } else {
+      this.$refs.multipleTable && this.$refs.multipleTable.clearSelection()
+    }
+  }
+
+  get uploadHeader () {
+    if (this.$store.state.system.lang === 'en') {
+      return { 'Accept-Language': 'en' }
+    } else {
+      return { 'Accept-Language': 'cn' }
+    }
+  }
+
+  get checkedUploadRules () {
+    return Object.keys(this.uploadRules).filter(item => this.uploadRules[item].status === 'error').length > 0
+  }
+
+  async editWhiteSql (sqlObj) {
+    // 编辑模式下,不需要再进入当前SQL的编辑
+    if (this.isEditSql && sqlObj.id === this.activeSqlObj.id) {
+      return
+    }
+    if (this.isEditSql) {
+      await kylinWarn(this.$t('editSqlTips'), {
+        type: 'warning',
+        closeOnClickModal: false,
+        confirmButtonText: this.$t('confirmEditSqlText')
+      })
+      await this.validateWhiteSql()
+    }
+    this.isEditSql = true
+    this.inputHeight = 382
+    if (sqlObj.capable) {
+      this.isWhiteErrorMessage = false
+      this.inputHeight = 382
+    } else {
+      this.isWhiteErrorMessage = true
+      this.inputHeight = 382 - 140
+    }
+    let formatterSql
+    if (this.sqlFormatterObj[sqlObj.id]) {
+      formatterSql = this.sqlFormatterObj[sqlObj.id]
+      this.whiteSql = formatterSql
+      this.$refs.whiteInputBox && this.$refs.whiteInputBox.$emit('input', formatterSql)
+    } else {
+      this.showLoading()
+      const res = await this.formatSql({ sqls: [sqlObj.sql] })
+      const data = await handleSuccessAsync(res)
+      formatterSql = data[0]
+      this.sqlFormatterObj[sqlObj.id] = formatterSql
+      this.$refs.whiteInputBox && this.$refs.whiteInputBox.$emit('input', formatterSql)
+      this.hideLoading()
+    }
+    this.activeSqlObj = sqlObj
+    this.isReadOnly = false
+  }
+
+  async activeSql (sqlObj) {
+    if (this.isEditSql) {
+      await kylinWarn(this.$t('editSqlTips'), {
+        type: 'warning',
+        closeOnClickModal: false,
+        confirmButtonText: this.$t('confirmEditSqlText')
+      })
+      await this.validateWhiteSql()
+    }
+    this.activeSqlObj = sqlObj
+    this.isEditSql = false
+    this.isReadOnly = true
+    if (sqlObj.capable) {
+      this.isWhiteErrorMessage = false
+      this.inputHeight = 479
+    } else {
+      this.isWhiteErrorMessage = true
+      this.inputHeight = 479 - 140
+      this.whiteMessages = sqlObj.sql_advices
+    }
+    let formatterSql
+    if (this.sqlFormatterObj[sqlObj.id]) {
+      formatterSql = this.sqlFormatterObj[sqlObj.id]
+      this.$refs.whiteInputBox && this.$refs.whiteInputBox.$emit('input', formatterSql)
+      this.whiteSql = formatterSql
+    } else {
+      this.showLoading()
+      const res = await this.formatSql({ sqls: [sqlObj.sql] })
+      const data = await handleSuccessAsync(res)
+      formatterSql = data[0]
+      this.sqlFormatterObj[sqlObj.id] = formatterSql
+      this.$refs.whiteInputBox && this.$refs.whiteInputBox.$emit('input', formatterSql)
+      this.hideLoading()
+    }
+  }
+
+  fileItemChange (file, fileList) {
+    if (this.errMsgInstance) {
+      this.errMsgInstance.close()
+    }
+    this.wrongFormatFile = []
+    for (const item in this.uploadRules) {
+      this.uploadRules[item].status = 'default'
+    }
+    this.checkUploadFiles(fileList)
+    this.flagFiles()
+  }
+
+  handleRemove (file, fileList) {
+    if (this.errMsgInstance) {
+      this.errMsgInstance.close()
+    }
+    this.messageInstance && this.messageInstance.close()
+    this.uploadItems = fileList
+    // let totalSize = 0
+    // this.uploadItems.forEach((item) => {
+    //   totalSize = totalSize + item.size
+    // })
+    // if (totalSize > 5 * 1024 * 1024) { // 后端限制不能大于5M
+    //   this.messageInstance = this.$message.warning(this.$t('filesSizeError'))
+    // }
+    for (const item in this.uploadRules) {
+      this.uploadRules[item].status = 'default'
+    }
+    this.wrongFormatFile = []
+    if (fileList.length) {
+      this.checkUploadFiles(fileList)
+      this.flagFiles()
+    }
+  }
+
+  selectable (row) {
+    return row.capable
+  }
+
+  cancelEdit (isErrorMes) {
+    this.isEditSql = false
+    this.inputHeight = isErrorMes ? 479 - 140 : 479
+    this.whiteSql = this.sqlFormatterObj[this.activeSqlObj.id]
+    this.activeSqlObj = null
+    this.isReadOnly = true
+  }
+
+  validateWhiteSql () {
+    return new Promise((resolve, reject) => {
+      this.validateLoading = true
+      this.validateWhite({ sql: this.whiteSql, project: this.currentSelectedProject }).then((res) => {
+        handleSuccess(res, (data, code) => {
+          if (code !== '000') {
+            return reject(new Error('error requset'))
+          }
+          this.validateLoading = false
+          if (data.capable) {
+            this.$message.success(this.$t('kylinLang.common.actionSuccess'))
+            this.whiteMessages = []
+            this.inputHeight = 479
+            this.isWhiteErrorMessage = false
+            this.isEditSql = false
+            this.isReadOnly = true
+            for (const key in this.whiteSqlData.data) {
+              if (this.whiteSqlData.data[key].id === this.activeSqlObj.id) {
+                this.whiteSqlData.data[key].sql = this.whiteSql
+                this.sqlFormatterObj[this.activeSqlObj.id] = this.whiteSql
+                if (!this.whiteSqlData.data[key].capable) {
+                  this.whiteSqlData.data[key].capable = true
+                  this.whiteSqlData.data[key].sql_advices = []
+                  this.whiteSqlData.capable_sql_num++
+                }
+                break
+              }
+            }
+            resolve()
+          } else {
+            this.whiteMessages = data.sql_advices
+            this.inputHeight = 479 - 140
+            this.isWhiteErrorMessage = true
+            reject(new Error('uncapable data'))
+          }
+        })
+      }, (res) => {
+        this.validateLoading = false
+        handleError(res)
+        reject(res)
+      })
+    })
+  }
+
+  checkUploadFiles (fileList) {
+    let totalSize = 0
+    this.uploadItems = fileList.map((item) => {
+      totalSize += item.size
+      item.raw && (item.raw.status = item.status)
+      return item.raw ? item.raw : item
+    })
+    const fileNames = fileList.map(it => it.name)
+    if (totalSize > 5 * 1024 * 1024) { // 后端限制不能大于5M
+      this.uploadRules.totalSize.status = 'error'
+    } else {
+      this.uploadRules.totalSize.status = 'success'
+    }
+    if (fileNames.filter(name => name.toLowerCase().indexOf('.txt') === -1 && name.toLowerCase().indexOf('.sql') === -1).length > 0) {
+      this.uploadRules.fileFormat.status = 'error'
+    } else {
+      this.uploadRules.fileFormat.status = 'success'
+    }
+  }
+
+  uploadErrorMsg () {
+    this.errMsgInstance = this.$message({
+      message: this.$t('uploadErrorMsg', { maxCount: this.favoriteImportSqlMaxSize }),
+      type: 'error'
+    })
+  }
+
+  submitFiles () {
+    const formData = new FormData() // 利用H5 FORMDATA 同时传输多文件和数据
+    this.uploadItems.filter((item) => {
+      return item.name.toLowerCase().indexOf('.txt') !== -1 || item.name.toLowerCase().indexOf('.sql') !== -1
+    }).forEach(file => {
+      formData.append('files', file)
+    })
+    this.importLoading = true
+    this.importSqlFiles({ project: this.currentSelectedProject, formData }).then((res) => {
+      handleSuccess(res, (data, code, status, msg) => {
+        if (!this.isShow) return
+        this.checkUploadFiles(this.uploadItems)
+        this.showUploadRules = true
+        this.importLoading = false
+        // this.uploadRules.sqlSizes.status = data.size > this.favoriteImportSqlMaxSize ? 'error' : 'success'
+        // this.uploadRules.unValidSqls.status = data.wrong_format_file && data.wrong_format_file.length ? 'error' : 'success'
+        if (data.size > this.favoriteImportSqlMaxSize) {
+          return this.uploadErrorMsg()
+        }
+        this.wrongFormatFile = data.wrong_format_file || []
+        if (Object.keys(this.uploadRules).filter(item => this.uploadRules[item].status === 'success').length === Object.keys(this.uploadRules).length) {
+          this.uploadFlag = 'step2'
+          this.whiteSqlData = data
+          this.selectAll()
+          this.whiteSqlDatasPageChange(0)
+          if (msg) {
+            this.$message.warning(msg)
+          }
+        }
+        this.flagFiles()
+      })
+    }, (res) => {
+      handleError(res)
+      this.importLoading = false
+    })
+  }
+
+  // 标记有问题的文件
+  flagFiles () {
+    this.$nextTick(() => {
+      const doms = this.$refs.sqlUpload ? this.$refs.sqlUpload.$el.querySelectorAll('.el-upload-list__item') : []
+      const indexs = []
+      for (const i in this.uploadItems) {
+        const target = this.uploadItems[i]
+        if (target.name.toLowerCase().indexOf('.txt') === -1 && target.name.toLowerCase().indexOf('.sql') === -1) {
+          indexs.push(+i)
+        }
+        if (this.wrongFormatFile.includes(target.name)) {
+          indexs.push(+i)
+        }
+      }
+      doms.forEach((item, index) => {
+        indexs.includes(index) && (doms[index].style.cssText = 'border: 1px solid #E73371;')
+        !indexs.includes(index) && (doms[index].style.cssText = '')
+      })
+    })
+  }
+
+  mounted () {
+  }
+}
+</script>
+<style lang="less">
+  @import '../../../assets/styles/variables.less';
+  .sql-upload {
+    .el-upload-list__item {
+      line-height: 2;
+      background: @regular-background-color;
+      .el-icon-document {
+        color: @text-disabled-color;
+      }
+      .el-icon-close {
+        color: @text-title-color;
+        top: 8px;
+      }
+    }
+  }
+  .importSqlDialog .el-dialog__body .upload-block .sql-upload .el-upload-list .el-upload-list__item:hover {
+    background-color: @base-background-color-1;
+  }
+  .importSqlDialog {
+    &.is-step3 {
+      .el-dialog__body {
+        padding: 20px 20px 0;
+      }
+    }
+    .query_panel_box {
+      border: none;
+    }
+    .ksd-null-pic-text {
+      margin: 122.5px 0;
+    }
+    .query-count {
+      color: @text-title-color;
+      line-height: 30px;
+      height: 30px;
+      display: inline-block;
+      .merge-sql-tip {
+        font-size: 12px;
+        color: @text-normal-color;
+        .divide {
+          margin-right: 10px;
+          margin-left: 5px;
+          color: @line-border-color3;
+        }
+        i {
+          margin-right: 5px;
+          color: @text-disabled-color;
+        }
+      }
+      .selected-item {
+        color: @text-normal-color;
+        font-size: 12px;
+        .el-icon-ksd-alert {
+          margin-right: 5px;
+          color: @text-disabled-color;
+        }
+      }
+    }
+    .tips {
+      span {
+        margin-left: 5px;
+        color: @text-normal-color;
+      }
+      i {
+        color: @text-disabled-color;
+      }
+    }
+    .el-icon-ksd-good_health {
+      color: @normal-color-1;
+      margin-right: 2px;
+      font-size: 14px;
+    }
+    .el-icon-ksd-error_01 {
+      color: @error-color-1;
+      margin-right: 2px;
+      font-size: 14px;
+    }
+    .el-dialog__body {
+      // min-height: 460px;
+      height: 460px;
+      .model-table {
+        .rename-error {
+          color: @error-color-1;
+          font-size: 12px;
+          line-height: 1.2;
+        }
+        .name-error {
+          .el-input__inner {
+            border-color: @error-color-1;
+          }
+        }
+      }
+      .import-table {
+        .cell {
+          height: 23px;
+        }
+        .active-row {
+          background-color: @base-color-9;
+        }
+        .el-table__row {
+          cursor: pointer;
+        }
+        .el-icon-ksd-table_edit,
+        .el-icon-ksd-table_delete {
+          &:hover {
+            color: @base-color;
+          }
+          &.is-disabled {
+            color: @color-text-disabled;
+          }
+        }
+      }
+      .new_sql_status {
+        border-color: @base-color;
+        .ace-chrome {
+          border-color: @base-color;
+          border-bottom-color: @text-secondary-color;
+        }
+      }
+      .operatorBox{
+        margin-top: 0;
+        padding: 10px;
+        display: block;
+        overflow: hidden;
+      }
+      .error_messages {
+        height: 130px;
+        border: 1px solid @ke-border-secondary;
+        border-radius: 2px;
+        font-size: 12px;
+        margin-top: 10px;
+        padding: 10px;
+        box-sizing: border-box;
+        overflow-y: auto;
+        .label {
+          color: @error-color-1;
+        }
+      }
+      .smyles_editor_wrap .smyles_dragbar {
+        height: 0;
+      }
+      .upload-block {
+        text-align: center;
+        margin: 0 auto;
+        img {
+          margin-top: 100px;
+        }
+        .text {
+          font-size: 14px;
+          color: @text-title-color;
+          line-height: 24px;
+          font-weight: bold;
+        }
+        .upload-size-tip {
+          width: 440px;
+          display: inline-block;
+          color: @text-normal-color;
+          font-size: 12px;
+          margin-top: 5px;
+        }
+        .el-upload {
+          margin-top: 15px;
+        }
+        .el-upload-list {
+          width: 300px;
+          margin: 0 auto;
+          text-align: left;
+          .el-upload-list__item {
+            .el-upload-list__item-name {
+              margin-right: 20px;
+              &:hover {
+                text-decoration: none;
+                color: inherit;
+              }
+            }
+            &:hover {
+              background-color: inherit;
+            }
+          }
+        }
+        .upload-rules {
+          max-width: 300px;
+          margin: auto;
+          background-color: @ke-background-color-base-1;
+          font-size: 12px;
+          margin-top: 10px;
+          padding: 5px 20px;
+          color: @text-normal-color;
+          line-height: 20px;
+          &.check-rules {
+            background-color: transparent;
+          }
+          .rule-list {
+            display: inline-block;
+            text-align: left;
+          }
+          .rule-item {
+            &.is-success {
+              color: @ke-color-success;
+            }
+            &.is-error {
+              color: @ke-color-danger;
+            }
+          }
+          .rule-info {
+            margin-top: 10px;
+            margin-left: -7px;
+            text-align: left;
+            .icon {
+              display: inline-block;
+            }
+            .tip-text {
+              display: inline-block;
+              width: calc(~'100% - 20px');
+              vertical-align: top;
+            }
+          }
+        }
+      }
+    }
+    .filter-tags {
+      margin-bottom: 10px;
+      padding: 6px 10px;
+      box-sizing: border-box;
+      position: relative;
+      background: @background-disabled-color;
+      .filter-tags-layout {
+        width: calc(~'100% - 100px');
+        display: inline-block;
+        line-height: 34px;
+      }
+      .el-tag {
+        margin-left: 5px;
+        &:first-child {
+          margin-left: 0;
+        }
+      }
+      .clear-all-filters {
+        position: absolute;
+        top: 8px;
+        right: 10px;
+        font-size: 14px;
+        color: @base-color;
+        cursor: pointer;
+      }
+    }
+    .recommendation-alert {
+      margin-bottom: 10px;
+    }
+    .review-sql-model {
+      height: 100%;
+      .upload-tabs {
+        height: calc(~'100% - 47px');
+        .el-tabs__header {
+          margin-bottom: 0;
+        }
+        .el-tabs__content {
+          height: calc(~'100% - 30px');
+          #pane-suggest {
+            height: 100%
+          }
+          #pane-origin {
+            height: 100%
+          }
+        }
+      }
+    }
+    .review-sql-model.new-model {
+      .ky-list-title {
+        padding: 4px 0;
+        box-sizing: border-box;
+        // border-bottom: 1px solid #dddddd;
+      }
+      .model-layout {
+        height: calc(~'100% - 30px');
+        // .model-table {
+        //   height: 100%;
+        //   .el-table__body-wrapper {
+        //     max-height: calc(~'100% - 36px');
+        //     overflow-y: auto;
+        //   }
+        // }
+      }
+    }
+    .review-sql-model.origin-model {
+      .ky-list-title {
+        padding: 4px 0;
+        box-sizing: border-box;
+        border-bottom: 1px solid @ke-border-secondary;
+      }
+      .model-layout {
+        height: calc(~'100% - 75px');
+      }
+    }
+  }
+</style>
diff --git a/kystudio/src/components/common/UploadSql/locales.js b/kystudio/src/components/common/UploadSql/locales.js
new file mode 100644
index 0000000000..83bbb25e8b
--- /dev/null
+++ b/kystudio/src/components/common/UploadSql/locales.js
@@ -0,0 +1,156 @@
+export default {
+  en: {
+    importSql: 'Import SQL',
+    modelFromSql: 'Add Model From SQL',
+    generateModel: 'Import SQL',
+    pleImport: 'Upload SQL Files',
+    sqlFiles: 'Upload',
+    sqlBox: 'SQL Box',
+    checkAll: 'Check All',
+    cancelAll: 'Uncheck All',
+    addTofavorite: 'Submit',
+    messages: 'Error Messages:',
+    suggestion: 'Suggestion:',
+    filesSizeError: 'The total file size cannot exceed 5 MB',
+    fileTypeError: 'Invalid file format。',
+    uploadFileTips: 'Please upload .txt or .sql file which\'s no larger than 5MB in total. Please use semicolon to separate multiple SQLs.',
+    submitConfirm: '{unCheckedSQL} validated SQL statements are not selected, do you want to ignore them and submit anyway?',
+    addSuccess: '{sqlsNum} new SQL statement(s) has(have) been imported successfully. {importedNum} of them has(have) been added to the waiting list',
+    end: '.',
+    existedMsg1: ', and {existedNum} of them is(are) duplicated',
+    existedMsg2: ' (where {blackNum} statement(s) has(have) been added to the black list and will not be accelerated)',
+    renameError: 'Model with the same name already exists. Please rename it.',
+    delSql: 'Are you sure you want to delete this SQL?',
+    delSqlTitle: 'Delete SQL',
+    recommendations: 'Recommendation',
+    index: 'Index',
+    convert: 'Convert to Recommendations',
+    noConvert: 'Create New Models',
+    newModelList: 'New Model',
+    renameTips: 'The automatically generated model name may exceed the character limit and be automatically truncated. It is recommended to modify them for better management and retrieval.',
+    newModelTips: 'You can select the new models you want and expand to see which SQL these models come from. Click the Submit button and the new models will be added to the model list, which you can view and edit on the model page.',
+    originModelTips: 'You can observe the number of new recommendations for existing models and expand to see which SQL these recommendations come from. The new recommendations have been added to the recommendation lists of the corresponding models, where you can accept the required recommendations.',
+    existedAnsweredModels: 'Some of the imported SQLs could be answered by existing models. Do you want to convert these SQLS to recommendations of the existing models? Otherwise, new models will be created.',
+    th_name: 'Name',
+    th_type: 'Type',
+    measure: 'Measure',
+    dimension: 'Dimension',
+    cc: 'Computed Columns',
+    th_column: 'Column',
+    th_parameter: 'Function Parameter',
+    th_expression: 'Expression',
+    th_function: 'Function',
+    uploadSizeTip: 'Please use semicolon to separate multiple SQLs. To avoid possible errors, please don\'t edit the uploaded file before the model is added.',
+    selectModelsAndRecommends: 'Selected {models} model(s), {recommends} recommendation(s).',
+    selectModelTips: 'Selected {models} model(s).',
+    selectRecommendTips: 'Selected {recommends} recommendation(s).',
+    sqlInfo: 'This model (recommendation) could be answered by how many imported SQLs',
+    recommendType: 'Type',
+    ADD: 'Add',
+    AGG: 'Aggregate Index',
+    TABLE: 'Table Index',
+    successCreateModels: 'Successfully added {models} model(s)',
+    successCreateModelsAndRecommends: 'Successfully added {models} model(s), accepted {recommends} recommendation(s), The added indexes would be ready for queries after being built.',
+    successCreateRecommends: 'Successfully accepted {recommends} recommendation(s), The added indexes would be ready for queries after being built.',
+    confirmCancel: 'Confirm to Cancel',
+    backToEdit: 'Continue Editing',
+    closeSqlDialogTip: 'Once cancel, all edits would be discarded. Are you sure you want to cancel?',
+    recommendsOverSizeTip: 'Recommendations can\'t be shown properly due to the extreme large amount. Please try selecting fewer SQLs to import at a time.',
+    recommendsOverSizeTitle: 'Can\'t Show All Recommendations',
+    mergeSqlTip: 'Similar SQLs might have been merged. Non-equal join condition are not supported. ',
+    preview: 'Preview',
+    model: 'Model',
+    sqlOverSizeTip: 'Up to 200 SQLs could be uploaded at a time. Please try selecting fewer files to upload.',
+    recommendationTip: 'Please note that the selected recommendations would be effective immediately after proceeding',
+    editSqlTips: 'You have unsaved edits. Do you want to continue?',
+    confirmEditSqlText: 'Save and Continue',
+    noRecommendsTip: 'No valid recommendation. Existing models might already include the same indexes.',
+    noModelDetailsTip: 'The selected model\'s dimension, measure, computed column, aggregate index would be shown here.',
+    noOriginDetailsTip: 'The selected recommendation\'s dimension, measure, computed column would be shown here.',
+    importSuccess: 'Imported successfully',
+    uploadRule1: 'Please upload .txt or .sql file',
+    uploadRule2: 'Valid SQLs shall be included in the file',
+    uploadRule3: 'The total file size can\'t be larger than 5 MB',
+    uploadRule4: 'Up to {size} SQLs could be uploaded at a time',
+    addBaseIndexCheckBox: 'Add Base Indexes',
+    baseIndexTips: 'Base indexes include all dimensions and measures of the model and automatically update as the model changes by default(Only batch data is supported for now)',
+    uploadErrorMsg: 'The included SQLs exceed the maximum limit ({maxCount}) at a time. Please check and upload again.'
+  },
+  'zh-cn': {
+    importSql: '导入 SQL 文件',
+    modelFromSql: 'SQL 建模',
+    generateModel: '确认导入的 SQL',
+    pleImport: '上传 SQL 文件',
+    sqlFiles: '上传',
+    sqlBox: 'SQL 窗口',
+    checkAll: '全选',
+    cancelAll: '取消全选',
+    addTofavorite: '提交',
+    messages: '错误信息:',
+    suggestion: '修改建议:',
+    filesSizeError: '总文件大小不能超过 5 MB',
+    fileTypeError: '不支持的文件格式!',
+    uploadFileTips: '请上传总量不超过 5MB 的 .txt 或 .sql 文件。多条 SQL 请使用英文分号隔开。',
+    submitConfirm: '还有 {unCheckedSQL} 条正确的 SQL 未被选中,是否忽略并直接提交?',
+    addSuccess: '共导入 {sqlsNum} 条查询。{importedNum} 条新增进入可加速列表',
+    end: '。',
+    existedMsg1: ',{existedNum} 条已存在',
+    existedMsg2: '(其中 {blackNum} 条已被加入黑名单,未能进入加速列表)',
+    renameError: '已存在同名的模型。',
+    delSql: '确定删除这条查询语句吗?',
+    delSqlTitle: '删除查询语句',
+    recommendations: '优化建议',
+    index: '索引',
+    convert: '转换为优化建议',
+    noConvert: '创建模型',
+    newModelList: '新增模型',
+    renameTips: '自动生成的模型名称可能超出字符限制自动截断,建议修改以便管理和检索。',
+    newModelTips: '您可以勾选需要的新模型,并展开查看该模型来自于哪些 SQL。点击提交按钮,所勾选的模型将会被加入模型列表,您可以在模型页面进行查看和编辑。',
+    originModelTips: '您可以查看已有模型的新增建议数量,并展开查看它们来自于哪些 SQL。新增优化建议已经被加入对应模型的优化建议列表中,您可以在对应模型的优化建议中选择并通过需要的建议。',
+    existedAnsweredModels: '当前已有模型可回答部分导入的 SQL。是否将这部分 SQL 转换成对应模型的优化建议?或对这部分SQL创建新的模型。',
+    th_name: '名称',
+    th_type: '类型',
+    th_column: '列',
+    th_parameter: '函数参数',
+    th_function: '函数',
+    th_expression: '表达式',
+    measure: '度量',
+    dimension: '维度',
+    cc: '可计算列',
+    uploadSizeTip: '多条 SQL 请使用英文分号隔开。建模完成前请勿修改已上传的文件,否则可能导致出错。',
+    selectModelsAndRecommends: '已选择 {models} 个模型,{recommends} 条优化建议',
+    selectModelTips: '已选择 {models} 个模型',
+    selectRecommendTips: '已选择 {recommends} 条优化建议',
+    sqlInfo: '表示该模型(优化建议)可被多少条导入SQL击中',
+    recommendType: '建议类型',
+    ADD: '新增',
+    AGG: '聚合索引',
+    TABLE: '明细索引',
+    successCreateModels: '成功添加 {models} 个模型',
+    successCreateModelsAndRecommends: '成功添加 {models} 个模型,接受 {recommends} 条优化建议,新增索引在构建之后可以服务于查询。',
+    successCreateRecommends: '成功接受 {recommends} 条优化建议,新增索引在构建之后可以服务于查询。',
+    confirmCancel: '确认取消',
+    backToEdit: '返回编辑',
+    closeSqlDialogTip: '一旦取消,将退出SQL建模,同时所有编辑会被丢弃。确定要取消吗?',
+    recommendsOverSizeTip: '优化建议超过显示上限。请尝试减少导入 SQL 的数量。',
+    recommendsOverSizeTitle: '优化建议无法显示',
+    mergeSqlTip: '相似的 SQL 会在此页面进行合并。非等值关联关系暂不支持。',
+    preview: '预览',
+    model: '模型',
+    sqlOverSizeTip: '一次最多可上传 200 条 SQL。请重新上传。',
+    recommendationTip: '请注意,所选的优化建议将立即生效',
+    editSqlTips: 'SQL 编辑尚未保存,是否继续?',
+    confirmEditSqlText: '保存并继续',
+    noRecommendsTip: '没有有效的优化建议。现有模型可能已包含相同的索引。',
+    noModelDetailsTip: '选中模型包含的维度、度量、可计算列、聚合索引等信息会显示在这里。',
+    noOriginDetailsTip: '选中优化建议包含的维度、度量、可计算列等信息会显示在这里。',
+    importSuccess: '导入成功',
+    uploadRule1: '请上传 .txt 或 .sql 文件',
+    uploadRule2: '文件包含有效的 SQL',
+    uploadRule3: '文件大小总量不超过 5MB',
+    uploadRule4: '一次最多可上传 {size} 条 SQL',
+    addBaseIndexCheckBox: '添加基础索引',
+    baseIndexTips: '基础索引包含模型全部维度和度量,默认随着模型变化自动更新(暂时仅支持批数据)',
+    uploadErrorMsg: '上传的 SQL 数量超过单次上限 ({maxCount}) ,请检查后重新上传。'
+  }
+}
diff --git a/kystudio/src/components/common/UploadSql/store.js b/kystudio/src/components/common/UploadSql/store.js
new file mode 100644
index 0000000000..052eb2ce4e
--- /dev/null
+++ b/kystudio/src/components/common/UploadSql/store.js
@@ -0,0 +1,50 @@
+const types = {
+  SHOW_MODAL: 'SHOW_MODAL',
+  HIDE_MODAL: 'HIDE_MODAL',
+  SET_MODAL: 'SET_MODAL',
+  SET_MODAL_FORM: 'SET_MODAL_FORM',
+  RESET_MODAL_FORM: 'RESET_MODAL_FORM',
+  CALL_MODAL: 'CALL_MODAL'
+}
+// 声明:初始state状态
+const initialState = JSON.stringify({
+  isShow: false,
+  isGenerateModel: false,
+  title: '',
+  callback: null
+})
+
+export default {
+  // state深拷贝
+  state: JSON.parse(initialState),
+  mutations: {
+    // 显示Modal弹窗
+    [types.SHOW_MODAL]: (state) => {
+      state.isShow = true
+    },
+    // 隐藏Modal弹窗
+    [types.HIDE_MODAL]: (state) => {
+      state.isShow = false
+    },
+    [types.SET_MODAL]: (state, payload) => {
+      state.isGenerateModel = payload.isGenerateModel
+      state.title = payload.title
+      state.callback = payload.callback
+    },
+    // 还原Modal中的值为初始值
+    [types.RESET_MODAL_FORM]: (state) => {
+      state.isGenerateModel = false
+    }
+  },
+  actions: {
+    [types.CALL_MODAL] ({ commit }, { isGenerateModel, title }) {
+      return new Promise(resolve => {
+        commit(types.SET_MODAL, { isGenerateModel, title, callback: resolve })
+        commit(types.SHOW_MODAL)
+      })
+    }
+  },
+  namespaced: true
+}
+
+export { types }
diff --git a/kystudio/src/components/setting/SettingBasic/SettingBasic.vue b/kystudio/src/components/setting/SettingBasic/SettingBasic.vue
index a5b89fbdd6..3b222eb006 100644
--- a/kystudio/src/components/setting/SettingBasic/SettingBasic.vue
+++ b/kystudio/src/components/setting/SettingBasic/SettingBasic.vue
@@ -15,6 +15,20 @@
         <div class="setting-label font-medium">Developer Info</div>
         <div class="setting-value fixed"> {{project.package_version}} [{{project.package_timestamp}}, {{project.git_commit}}]</div>
       </div>
+      <div class="setting-item">
+        <span class="setting-label font-medium">
+          {{$t('enableSemiAutomatic')}}
+          <!-- <span class="beta-label">BETA</span> -->
+        </span>
+        <span class="setting-value fixed">
+          <el-switch
+            v-model="form.semi_automatic_mode"
+            :active-text="$t('kylinLang.common.OFF')"
+            :inactive-text="$t('kylinLang.common.ON')">
+          </el-switch>
+        </span>
+        <div class="setting-desc">{{$t('enableSemiAutomaticDesc')}}</div>
+      </div>
       <div class="setting-item clearfix">
         <div class="setting-label font-medium">{{$t('description')}}</div>
         <div class="setting-value">{{project.description}}</div>
@@ -137,6 +151,111 @@
         </div>
       </el-form>
     </EditableBlock>
+    <EditableBlock
+      v-if="$store.state.project.isSemiAutomatic"
+      ref="accelerationRuleSettings"
+      :header-content="$t('accelerationRuleSettings')"
+      :is-keep-editing="true"
+      :is-edited="isFormEdited(form, 'acceleration-rule-settings')"
+      @submit="(scb, ecb) => handleSubmit('acceleration-rule-settings', scb, ecb)"
+      @cancel="(scb, ecb) => handleResetForm('acceleration-rule-settings', scb, ecb)">
+      <el-form ref="rulesForm" :rules="rulesSettingRules" :model="rulesObj" size="medium" class="ruleSetting">
+        <div class="conds">
+          <div class="conds-title">
+            <span class="setting-label font-medium">{{$t('querySubmitter')}}</span><span class="ksd-fs-12">
+            <el-switch v-model="rulesObj.submitter_enable" :active-text="$t('kylinLang.common.OFF')" :inactive-text="$t('kylinLang.common.ON')"></el-switch></span>
+          </div>
+          <div class="conds-content">
+            <div class="ksd-fs-12 ksd-mt-5">{{$t('querySubmitterTips')}}</div>
+            <div class="vip-users-block">
+              <el-form-item prop="users">
+                <div class="ksd-mt-8 conds-title"><i class="el-icon-ksd-table_admin"></i> User</div>
+                <el-select v-model="rulesObj.users" v-event-stop :popper-append-to-body="false" filterable remote :remote-method="remoteMethod" :loading="loading" size="medium" :placeholder="rulesObj.users.length ? '' : $t('kylinLang.common.pleaseSelectOrSearch')" class="ksd-mt-5" multiple style="width:100%">
+                  <span slot="prefix" class="el-input__icon el-ksd-icon-search_22" v-if="!rulesObj.users.length"></span>
+                  <el-option v-for="item in filterSubmitterUserOptions" :key="item" :label="item" :value="item"></el-option>
+                </el-select>
+              </el-form-item>
+              <el-form-item prop="user_groups">
+                <div class="ksd-mt-8 conds-title"><i class="el-icon-ksd-table_group"></i> User Group</div>
+                <el-select v-model="rulesObj.user_groups" v-event-stop :popper-append-to-body="false" filterable size="medium" :placeholder="rulesObj.user_groups.length ? '' : $t('kylinLang.common.pleaseSelectOrSearch')" class="ksd-mt-5" multiple style="width:100%">
+                  <span slot="prefix" class="el-input__icon el-ksd-icon-search_22" v-if="!rulesObj.user_groups.length"></span>
+                  <el-option v-for="item in allSubmittersOptions.group" :key="item" :label="item" :value="item"></el-option>
+                </el-select>
+              </el-form-item>
+            </div>
+          </div>
+        </div>
+        <div class="conds">
+          <div class="conds-title">
+            <span class="setting-label font-medium">{{$t('queryDuration')}}</span>
+          </div>
+          <div class="conds-content clearfix">
+            <div class="duration-rule ksd-mt-8 ksd-fs-12">
+              {{$t('from')}}
+              <el-form-item prop="min_duration" style="display: inline-block;">
+                <el-input v-model.trim="rulesObj.min_duration" v-number="rulesObj.min_duration" size="small" :class="['rule-setting-input', durationError && 'is-error']" @blur="$refs.rulesForm.validateField('min_duration')"></el-input>
+              </el-form-item>
+              {{$t('to')}}
+              <el-form-item prop="max_duration" style="display: inline-block;">
+                <el-input v-model.trim="rulesObj.max_duration" v-number="rulesObj.max_duration" size="small" :class="['rule-setting-input', durationError && 'is-error']" @blur="$refs.rulesForm.validateField('max_duration')"></el-input>
+              </el-form-item>
+              {{$t('secondes')}}
+            </div>
+            <span class="error-msg" v-if="durationError && durationErrorMsg">{{durationErrorMsg}}</span>
+          </div>
+        </div>
+        <div class="conds">
+          <div class="conds-title">
+            <span class="setting-label font-medium">{{$t('hitRules')}}</span>
+          </div>
+          <div class="conds-content clearfix">
+            <div class="ksd-fs-12 ksd-mt-8">
+              {{$t('ruleTips')}}
+              <el-tooltip effect="dark" :content="$t('ruleTooltips')" placement="top"><i class="el-ksd-icon-more_info_16 icon ksd-fs-16"></i></el-tooltip>
+            </div>
+            <div class="ksd-fs-12 ksd-mt-8">
+              {{$t('timeFrame')}}
+              <el-form-item prop="effective_days" class="inline-form-item">
+                <el-input-number style="width:100px;" input-enabled v-model="rulesObj.effective_days" size="small" :min="1" :max="30" ></el-input-number>
+              </el-form-item>
+              {{$t('days')}}
+            </div>
+            <div class="ksd-fs-12 ksd-mt-8">
+              {{$t('hitNums')}}
+              <el-form-item prop="min_hit_count" class="inline-form-item">
+                <el-input style="width:100px;" v-model.trim="rulesObj.min_hit_count" v-number4="rulesObj.min_hit_count" size="small"></el-input>
+              </el-form-item>
+              {{$t('hitNumTips')}}
+            </div>
+          </div>
+        </div>
+        <div class="conds">
+          <div class="conds-title">
+            <span class="setting-label font-medium">{{$t('optimizationSuggestions')}}</span>
+          </div>
+          <div class="conds-content clearfix">
+            <div class="conds-content clearfix">
+              <div class="ksd-mt-8 ksd-fs-12">
+                {{$t('suggestionTip1')}}
+                <el-form-item prop="recommendations_value" style="display: inline-block;">
+                  <el-select v-model="rulesObj.recommendations_value" v-event-stop size="mini" :disabled="!rulesObj.recommendation_enable" :placeholder="$t('kylinLang.common.pleaseSelectOrSearch')" class="ksd-mt-5" style="width:70px">
+                    <el-option v-for="item in [10, 20, 50, 100]" :key="item" :label="item" :value="item"></el-option>
+                  </el-select>
+                </el-form-item>
+                <span v-html="$t('suggestionTip2')"></span>
+              </div>
+            </div>
+            <div class="ksd-fs-12 ksd-mt-12">
+              {{$t('recommendationFrequency')}}
+              <el-form-item prop="update_frequency" class="inline-form-item">
+                <el-input-number style="width:100px;" input-enabled v-model="rulesObj.update_frequency" size="small" :min="1"></el-input-number>
+              </el-form-item>
+              <span v-html="$t('recommendationFrequencyTips')"></span>
+            </div>
+          </div>
+        </div>
+      </el-form>
+    </EditableBlock>
     <!-- 屏蔽列设置 -->
     <EditableBlock
       :header-content="$t('excludeRuleSettings')"
@@ -206,7 +325,7 @@ import { mapActions, mapGetters, mapMutations, mapState } from 'vuex'
 import { Component, Watch } from 'vue-property-decorator'
 
 import locales from './locales'
-import { handleError, handleSuccessAsync, objectClone, ArrayFlat, kylinConfirm } from '../../../util'
+import { handleError, handleSuccess, handleSuccessAsync, objectClone, ArrayFlat, kylinConfirm } from '../../../util'
 import { lowUsageStorageTypes, autoMergeTypes, volatileTypes, validate, initialFormValue, _getProjectGeneralInfo, _getSegmentSettings, _getPushdownConfig, _getExcludeColumnConfig, _getStorageQuota, _getIndexOptimization, _getRetentionRangeScale } from './handler'
 import { retentionTypes } from '../handler'
 import { pageCount, pageRefTags } from '../../../config'
@@ -244,6 +363,7 @@ import SourceAuthorityForm from '../../common/DataSourceModal/SourceJDBC/SourceA
       updateStorageQuota: 'UPDATE_STORAGE_QUOTA',
       updateIndexOptimization: 'UPDATE_INDEX_OPTIMIZATION',
       resetConfig: 'RESET_PROJECT_CONFIG',
+      getFavoriteRules: 'GET_FAVORITE_RULES',
       getUserAndGroups: 'GET_USER_AND_GROUPS',
       updateFavoriteRules: 'UPDATE_FAVORITE_RULES',
       fetchDBandTables: 'FETCH_DB_AND_TABLES',
@@ -270,7 +390,40 @@ export default class SettingBasic extends Vue {
   retentionTypes = retentionTypes
   form = initialFormValue
   storageQuotaSize = 0
+  rulesSettingRules = {
+    count_value: [{ validator: this.validatePass, trigger: 'blur' }],
+    min_duration: [{ validator: this.validatePass, trigger: 'blur' }],
+    max_duration: [{ validator: this.validatePass, trigger: 'blur' }],
+    effective_days: [{ validator: this.validatePass, trigger: 'blur' }],
+    min_hit_count: [{ validator: this.validatePass, trigger: 'blur' }],
+    update_frequency: [{ validator: this.validatePass, trigger: 'blur' }]
+  }
+
+  allSubmittersOptions = {
+    user: [],
+    group: []
+  }
   loading = false
+  filterUsers = []
+  rulesObj = {
+    count_enable: true,
+    count_value: 0,
+    submitter_enable: true,
+    users: [],
+    user_groups: [],
+    duration_enable: false,
+    min_duration: 0,
+    max_duration: 0,
+    recommendation_enable: true,
+    recommendations_value: 20,
+    effective_days: 2,
+    min_hit_count: 30,
+    update_frequency: 2
+  }
+
+  rulesAccelerationDefault = {}
+  durationError = false
+  durationErrorMsg = ''
   dbInfoFilter = {
     project_name: '',
     source_type: 9,
@@ -290,6 +443,10 @@ export default class SettingBasic extends Vue {
   }
   excludeColumnsTablesSize = 1
 
+  created () {
+    this.rulesAccelerationDefault = { ...this.rulesObj }
+  }
+
   get retentionRangeScale () {
     return _getRetentionRangeScale(this.form).toLowerCase()
   }
@@ -375,6 +532,7 @@ export default class SettingBasic extends Vue {
   @Watch('form', { deep: true })
   @Watch('project', { deep: true })
   @Watch('rulesObj', { deep: true })
+  @Watch('rulesAccelerationDefault', { deep: true })
   onFormChange () {
     const basicSetting = this.isFormEdited(this.form, 'basic-info') || this.isFormEdited(this.form, 'datasource-info') || this.isFormEdited(this.form, 'segment-settings')
     this.$emit('form-changed', { basicSetting })
@@ -394,6 +552,13 @@ export default class SettingBasic extends Vue {
     if (this.$store.state.project.projectExcludeTableConfig) {
       this.getExcludeColumns()
     }
+    if (this.$store.state.project.isSemiAutomatic) {
+      this.getAccelerationRules()
+      // this.getDbAndTablesInfo()
+    }
+    if ('moveTo' in this.$route.query && this.$route.query.moveTo === 'index-suggest-setting') {
+      this.$refs.accelerationRuleSettings && this.$refs.accelerationRuleSettings.$el.scrollIntoView && this.$refs.accelerationRuleSettings.$el.scrollIntoView()
+    }
   }
   handleCheckMergeRanges (value) {
     if (value.length > 0) {
@@ -452,8 +617,37 @@ export default class SettingBasic extends Vue {
       switch (type) {
         case 'basic-info': {
           const submitData = _getProjectGeneralInfo(this.form)
-          await this.updateProjectGeneralInfo(submitData)
-          break
+          if (!submitData.semi_automatic_mode) {
+            await this.callGlobalDetailDialog({
+              msg: this.$t('turnOffTips'),
+              title: this.$t('turnOff') + this.$t('enableSemiAutomatic'),
+              dialogType: 'warning',
+              isBeta: false,
+              wid: '600px',
+              showDetailBtn: false,
+              dangerouslyUseHTMLString: true,
+              needConcelReject: true,
+              submitText: this.$t('confirmClose')
+            })
+            await this.updateProjectGeneralInfo(submitData); break
+          } else {
+            await this.callGlobalDetailDialog({
+              msg: this.$t('turnOnTips'),
+              title: this.$t('turnOn') + this.$t('enableSemiAutomatic'),
+              dialogType: 'warning',
+              isBeta: false,
+              wid: '400px',
+              isCenterBtn: true,
+              showDetailBtn: false,
+              dangerouslyUseHTMLString: true,
+              needConcelReject: true,
+              submitText: this.$t('confirmOpen')
+            })
+            await this.updateProjectGeneralInfo(submitData)
+            this.getAccelerationRules()
+            // this.getDbAndTablesInfo() // 开启智能推荐模式,拉取数据源表
+            break
+          }
         }
         case 'datasource-info': {
           if (!this.form.JDBCConnectSetting.length) return errorCallback()
@@ -518,6 +712,13 @@ export default class SettingBasic extends Vue {
             return errorCallback()
           }
         }
+        case 'acceleration-rule-settings': {
+          if (await this.$refs.rulesForm.validate() && !this.durationError) {
+            await this.saveAccelerationRule()
+          } else {
+            return errorCallback()
+          }
+        }
       }
       successCallback()
       await this.$emit('reload-setting')
@@ -599,6 +800,14 @@ export default class SettingBasic extends Vue {
           this.$refs['setting-index-optimization'].clearValidate()
           break
         }
+        case 'acceleration-rule-settings': {
+          const res = await this.resetConfig({ project: this.currentSelectedProject, reset_item: 'favorite_rule_config' })
+          const data = await handleSuccessAsync(res)
+          const { favorite_rules } = data
+          this.rulesObj = { ...this.rulesObj, ...favorite_rules }
+          this.rulesAccelerationDefault = { ...this.rulesAccelerationDefault, ...favorite_rules }
+          this.$refs.rulesForm.resetFields()
+        }
       }
       successCallback()
       this.$emit('reload-setting')
@@ -626,9 +835,73 @@ export default class SettingBasic extends Vue {
         return JSON.stringify(_getStorageQuota(form)) !== JSON.stringify(_getStorageQuota(project))
       case 'index-optimization':
         return JSON.stringify(_getIndexOptimization(form)) !== JSON.stringify(_getIndexOptimization(project))
+      case 'acceleration-rule-settings':
+        return JSON.stringify(this.rulesAccelerationDefault) !== JSON.stringify(this.rulesObj)
     }
   }
 
+  // 获取优化建议规则
+  getAccelerationRules () {
+    if (this.currentSelectedProject) {
+      this.getFavoriteRules({ project: this.currentSelectedProject }).then((res) => {
+        handleSuccess(res, (data) => {
+          this.rulesObj = { ...data }
+          this.rulesAccelerationDefault = { ...data }
+        })
+      }).catch((res) => {
+        handleError(res)
+      })
+      this.getUserAndGroups().then((res) => {
+        handleSuccess(res, (data) => {
+          this.allSubmittersOptions = data
+          this.filterUsers = this.allSubmittersOptions.user
+        }, (res) => {
+          handleError(res)
+        })
+      })
+    }
+  }
+
+  remoteMethod (query) {
+    if (query !== '') {
+      this.loading = true
+      setTimeout(() => {
+        this.loading = false
+        this.filterUsers = this.allSubmittersOptions.user.filter(item => {
+          return item.toLowerCase()
+            .indexOf(query.toLowerCase()) > -1
+        })
+      }, 0)
+    } else {
+      this.filterUsers = this.allSubmittersOptions.user
+    }
+  }
+
+  get filterSubmitterUserOptions () {
+    const filterUsers = objectClone(this.filterUsers)
+    return filterUsers.splice(0, 500)
+  }
+
+  // 保存优化建议规则
+  saveAccelerationRule () {
+    return new Promise((resolve, reject) => {
+      const submitData = objectClone(this.rulesObj)
+      submitData.min_duration = +submitData.min_duration
+      submitData.max_duration = +submitData.max_duration
+      // 换成次数字段了,不需要除于 100
+      // submitData.freqValue = submitData.freqValue / 100
+      this.updateFavoriteRules({ ...submitData, ...{ project: this.currentSelectedProject } }).then((res) => {
+        handleSuccess(res, (data) => {
+          this.rulesAccelerationDefault = { ...this.rulesAccelerationDefault, ...this.rulesObj }
+        })
+        resolve()
+      }, (res) => {
+        handleError(res)
+        reject(res)
+      })
+    })
+  }
+
   changeDurationEnable (val) {
     !val && (this.durationError = val)
   }
diff --git a/kystudio/src/components/setting/SettingBasic/locales.js b/kystudio/src/components/setting/SettingBasic/locales.js
index 4fa8d89786..8cbd0b18d0 100644
--- a/kystudio/src/components/setting/SettingBasic/locales.js
+++ b/kystudio/src/components/setting/SettingBasic/locales.js
@@ -77,7 +77,7 @@ export default {
     from: 'Generate recommendations for the queries of which the duration is between',
     to: 'to',
     secondes: 'second(s)',
-    acclerationRuleSettings: 'Recommendation Preferences',
+    accelerationRuleSettings: 'Recommendation Preferences',
     optimizationSuggestions: 'Limit of Recommendations for Adding Index',
     suggestionTip1: 'Up to',
     suggestionTip2: 'recommendations for adding indexes would be generated per time. Recommendations would be updated every 2 days by default. The frequency could be configured. Check <a class="ky-a-like" href="" target="_blank">user manual</a> for details.',
diff --git a/kystudio/src/components/studio/StudioModel/ModelList/ModelActions/modelActions.vue b/kystudio/src/components/studio/StudioModel/ModelList/ModelActions/modelActions.vue
index 9541ecb172..544cf9bdca 100644
--- a/kystudio/src/components/studio/StudioModel/ModelList/ModelActions/modelActions.vue
+++ b/kystudio/src/components/studio/StudioModel/ModelList/ModelActions/modelActions.vue
@@ -262,6 +262,9 @@ import locales from './locales'
       'currentSelectedProject'
     ])
   },
+  inject: [
+    'isAdvancedOperatorUser'
+  ],
   methods: {
     ...mapActions({
       fetchSegments: 'FETCH_SEGMENTS',
diff --git a/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregate/sub/recommendations.vue b/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregate/sub/recommendations.vue
new file mode 100644
index 0000000000..4e424a7fea
--- /dev/null
+++ b/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregate/sub/recommendations.vue
@@ -0,0 +1,1082 @@
+<template>
+  <el-card class="recommendations-card">
+    <div slot="header">
+      <p>{{$t('recommendations')}}</p>
+    </div>
+    <div class="detail-content">
+      <p class="title-tip ksd-mb-8">{{$t('recommendationsTip1')}}<span v-if="$lang !== 'en'&&datasourceActions.includes('acceRuleSettingActions')">{{$t('recommendationsTip2')}}</span><a href="javascript:void(0);" v-if="datasourceActions.includes('acceRuleSettingActions')" @click="jumpToSetting">{{$t('modifyRules')}}</a><span v-if="$lang === 'en' && datasourceActions.includes('acceRuleSettingActions')">{{$t('recommendationsTip2')}}</span></p>
+      <el-alert :title="$t('realTimeModelAcceptRecommendTips')" type="tip" show-icon v-if="modelDesc.model_type === 'STREAMING'" />
+      <div class="ksd-fleft ksd-mb-10 ksd-mt-10 ksd-fs-12" >
+        <el-button text :disabled="!selectedList.length" @click="batchAccept" type="primary" icon="el-ksd-icon-confirm_22">{{$t('accept')}}</el-button><el-button text :disabled="!selectedList.length" type="primary" @click="batchDelete" icon="el-ksd-icon-table_delete_22">{{$t('delete')}}</el-button>
+      </div>
+      <div class="search-contain ksd-fright ksd-mt-10">
+        <el-input class="search-input" v-model.trim="recommendationsList.key" size="medium" :placeholder="$t('searchContentOrIndexId')" prefix-icon="el-ksd-icon-search_22" v-global-key-event.enter.debounce="searchRecommendation" @clear="searchRecommendation"></el-input>
+      </div>
+      <el-table
+        :data="recommendationsList.list"
+        class="recommendations-table clearfix"
+        size="medium"
+        max-height="350"
+        v-loading="loadingRecommends"
+        :empty-text="emptyText"
+        @selection-change="handleSelectionChange"
+        @sort-change="changeSort"
+      >
+        <el-table-column type="selection" width="44"></el-table-column>
+        <el-table-column
+          width="160"
+          :label="$t('th_recommendType')"
+          :filters="typeList.map(item => ({text: $t(item), value: item}))"
+          :filtered-value="checkedStatus"
+          filter-icon="el-ksd-icon-filter_22"
+          :show-multiple-footer="false"
+          :filter-change="(v) => filterType(v, 'checkedStatus')"
+          prop="type"
+          show-overflow-tooltip>
+          <template slot-scope="scope">
+            <el-tag size="mini" :type="scope.row.type.split('_')[0] === 'ADD' ? 'success' : 'danger'" v-if="['ADD', 'REMOVE'].includes(scope.row.type.split('_')[0])">{{scope.row.type.split('_')[0] === 'ADD' ? $t('newAdd') : $t('delete')}}</el-tag>
+            <span style="position:relative;top:1px;">{{$t(scope.row.type.split('_')[1])}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          width="120"
+          prop="id"
+          label="Index ID">
+          <template slot-scope="scope">
+            <span v-if="scope.row.type !== 'ADD_AGG_INDEX' && scope.row.type !== 'ADD_TABLE_INDEX'">{{$t(scope.row.index_id)}}</span>
+          </template>
+        </el-table-column>
+        <!-- <el-table-column
+          width="110"
+          :label="$t('th_source')"
+          :filters="source.map(item => ({text: $t(item), value: item}))"
+          :filtered-value="sourceCheckedStatus"
+          filter-icon="el-icon-ksd-filter"
+          :show-multiple-footer="false"
+          :filter-change="(v) => filterType(v, 'sourceCheckedStatus')">
+          <template slot-scope="scope">
+            {{$t(scope.row.source)}}
+          </template>
+        </el-table-column> -->
+        <el-table-column
+          width="110"
+          prop="data_size"
+          :label="$t('th_dataSize')"
+          sortable>
+          <template slot-scope="scope">
+            {{formatDataSize(scope.row.data_size)}}
+          </template>
+        </el-table-column>
+        <el-table-column
+          width="120"
+          prop="usage"
+          :label="$t('th_useCount')"
+          :info-tooltip="$t('usedCountTip')"
+          sortable>
+        </el-table-column>
+        <el-table-column
+          width='220'
+          prop="last_modified"
+          :label="$t('th_updateDate')"
+          sortable
+          show-overflow-tooltip>
+          <template slot-scope="scope">
+            {{transToGmtTime(scope.row.last_modified)}}
+          </template>
+        </el-table-column>
+        <el-table-column align="left" :label="$t('indexContent')">
+          <template slot-scope="scope">
+            <el-popover
+              ref="index-content-popover"
+              placement="top"
+              trigger="hover"
+              popper-class="col-index-content-popover">
+              <div class="index-content" slot="reference">{{getRowContentList(scope.row).map(it => it.name).slice(0, 20).join(', ')}}</div>
+              <template>
+                <p class="popover-header"><b>{{$t('indexesContent')}}</b><el-button v-if="getRowContentList(scope.row).length > 20" type="primary" text class="view-more-btn ksd-fs-12" @click="showDetail(scope.row)">{{$t('viewIndexDetails')}}<i class="el-icon-ksd-more_02 ksd-fs-12"></i></el-button></p>
+                <p style="white-space: pre-wrap;">{{getRowContentList(scope.row).map(it => it.name).slice(0, 20).join('\n')}}</p>
+                <el-button v-if="getRowContentList(scope.row).length > 20" class="ksd-fs-12" type="primary" text @click="showDetail(scope.row)">{{$t('viewAll')}}</el-button>
+              </template>
+            </el-popover>
+            <span class="detail-icon el-ksd-icon-view_16" @click="showDetail(scope.row)"></span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          width='120'
+          :label="$t('th_note')">
+          <div slot-scope="scope" class="col-tab-note">
+            <!-- <el-tag class="th-note-tag" size="small" type="warning">查询历史</el-tag> -->
+            <template v-if="'recommendation_source' in scope.row.memo_info">
+              <el-tooltip class="item" effect="dark" :content="removeReasonTip(scope)" placement="top">
+                <el-tag class="th-note-tag" size="small" type="warning">{{$t(scope.row.memo_info.recommendation_source)}}</el-tag>
+              </el-tooltip>
+            </template>
+          </div>
+        </el-table-column>
+        <el-table-column :label="$t('kylinLang.common.action')" width="83" fixed="right">
+          <template slot-scope="scope">
+            <common-tip :content="$t('accept')">
+              <i class="el-icon-ksd-accept ksd-ml-5" @click="doConfirm([scope.row])"></i>
+            </common-tip>
+            <common-tip :content="$t('delete')">
+              <i class="el-icon-ksd-table_delete ksd-ml-5" @click="removeIndex(scope.row)"></i>
+            </common-tip>
+          </template>
+        </el-table-column>
+      </el-table>
+      <kylin-pager class="ksd-center ksd-mtb-10" ref="indexPager" :totalSize="recommendationsList.totalSize" :refTag="pageRefTags.recommendationsPager" :perPageSize="recommendationsList.page_size" :curPage="recommendationsList.page_offset+1" v-on:handleCurrentChange='pageCurrentChange'></kylin-pager>
+    </div>
+    <!-- 索引详情 -->
+    <el-dialog
+      class="layout-details"
+      :title="indexDetailTitle"
+      width="880px"
+      :append-to-body="true"
+      :close-on-press-escape="false"
+      :close-on-click-modal="false"
+      @close="showIndexDetail = false"
+      :visible="true"
+      v-if="showIndexDetail"
+    >
+      <el-table
+        border
+        v-loading="loadingDetails"
+        :data="detailData"
+        class="index-details-table"
+        size="medium"
+        :fit="false"
+        :empty-text="emptyText"
+        style="width: 100%"
+        :cell-class-name="getCellClassName"
+      >
+        <el-table-column width="34" type="expand">
+          <template slot-scope="scope" v-if="scope.row.content">
+            <template v-if="scope.row.type === 'cc'">
+              <p><span class="label">{{$t('th_expression')}}:</span>{{scope.row.content}}</p>
+            </template>
+            <template v-if="scope.row.type === 'dimension'">
+              <p><span class="label">{{$t('th_column')}}:</span>{{JSON.parse(scope.row.content).column}}</p>
+              <p><span class="label">{{$t('th_dataType')}}:</span>{{JSON.parse(scope.row.content).data_type}}</p>
+            </template>
+            <template v-if="scope.row.type === 'measure'">
+              <p><span class="label">{{$t('th_column')}}:</span>{{JSON.parse(scope.row.content).name}}</p>
+              <p><span class="label">{{$t('th_function')}}:</span>{{JSON.parse(scope.row.content).function.expression}}</p>
+              <p><span class="label">{{$t('th_parameter')}}:</span>{{JSON.parse(scope.row.content).function.parameters}}</p>
+            </template>
+          </template>
+        </el-table-column>
+        <el-table-column type="index" :label="$t('order')" width="50"></el-table-column>
+        <el-table-column :label="$t('th_name')" width="550">
+          <template slot-scope="scope">
+            <span class="column-name" :title="scope.row.name" :style="{width: scope.row.add ? 'calc(100% - 50px)' : '100%'}">{{scope.row.name}}</span>
+            <el-tag class="add-tag" size="mini" type="success" v-if="scope.row.add">{{$t('newAdd')}}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="type" :label="$t('th_type')" width="70" show-overflow-tooltip>
+          <template slot-scope="scope">
+            {{$t(scope.row.type)}}
+          </template>
+        </el-table-column>
+        <el-table-column prop="cardinality" :label="$t('cardinality')" info-icon="el-ksd-icon-more_info_22" width="130" :info-tooltip="$t('cardinalityColumnTips')" sortable>
+          <template slot-scope="scope">
+            <span v-if="scope.row.cardinality">{{scope.row.cardinality}}</span>
+            <span v-else><i class="no-data_placeholder">NULL</i></span>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div slot="footer" class="dialog-footer ky-no-br-space">
+        <el-button size="medium" @click="showIndexDetail = false">{{$t('kylinLang.common.cancel')}}</el-button>
+        <el-button type="primary" size="medium" icon="el-ksd-icon-confirm_22" @click="acceptLayout" :loading="accessLoading">{{$t('accept')}}</el-button>
+      </div>
+    </el-dialog>
+    <!-- cc/度量/维度更名 -->
+    <el-dialog
+      class="layout-details"
+      :title="$t('validateTitle')"
+      width="480px"
+      :append-to-body="true"
+      :close-on-press-escape="false"
+      :close-on-click-modal="false"
+      @close="showValidate = false"
+      :visible="true"
+      v-if="showValidate"
+    >
+      <p>{{$t('validateModalTip')}}</p>
+      <el-table
+        nested
+        border
+        :data="getValidateList"
+        class="validate-table"
+        size="medium"
+        :empty-text="emptyText"
+      >
+        <el-table-column width="34" type="expand">
+          <template slot-scope="scope">
+            <template v-if="scope.row.type === 'cc'">
+              <p><span class="label">{{$t('th_expression')}}:</span>{{scope.row.content}}</p>
+            </template>
+            <template v-if="scope.row.type === 'dimension'">
+              <p><span class="label">{{$t('th_column')}}:</span>{{JSON.parse(scope.row.content).column}}</p>
+              <p><span class="label">{{$t('th_dataType')}}:</span>{{JSON.parse(scope.row.content).data_type}}</p>
+            </template>
+            <template v-if="scope.row.type === 'measure'">
+              <p><span class="label">{{$t('th_column')}}:</span>{{JSON.parse(scope.row.content).name}}</p>
+              <p><span class="label">{{$t('th_function')}}:</span>{{JSON.parse(scope.row.content).function.expression}}</p>
+              <p><span class="label">{{$t('th_parameter')}}:</span>{{JSON.parse(scope.row.content).function.parameters}}</p>
+            </template>
+          </template>
+        </el-table-column>
+        <el-table-column :label="$t('th_name')" width="300" show-overflow-tooltip>
+          <template slot-scope="scope">
+            <el-form :model="scope.row" :ref="`validateForm_${scope.row.item_id}`" :rules="scope.row.type !== 'cc' ? rules : rulesCC">
+              <el-form-item prop="name">
+                <el-tooltip class="item" effect="dark" :content="$t('usedInOtherModel')" placement="top" :disabled="scope.row.type === 'cc' ? !scope.row.cross_model : true">
+                  <el-input :class="{'is-error': sameNameErrorStatus(scope.row.item_id)}" v-model="scope.row.name" size="mini" @change="changeInputContent" :disabled="scope.row.type === 'cc' ? scope.row.cross_model : false"></el-input>
+                </el-tooltip>
+                <p class="error-text" v-if="sameNameErrorStatus(scope.row.item_id)">{{$t('sameNameTips')}}</p>
+              </el-form-item>
+            </el-form>
+          </template>
+        </el-table-column>
+        <el-table-column prop="type" :label="$t('th_type')" show-overflow-tooltip>
+          <template slot-scope="scope">
+            {{$t(scope.row.type)}}
+          </template>
+        </el-table-column>
+      </el-table>
+      <div slot="footer" class="dialog-footer ky-no-br-space">
+        <el-button size="medium" @click="showValidate = false">{{$t('kylinLang.common.cancel')}}</el-button>
+        <el-button type="primary" size="medium" icon="" @click="addLayout" :loading="isLoading">{{$t('add')}}</el-button>
+      </div>
+    </el-dialog>
+  </el-card>
+</template>
+
+<script>
+import Vue from 'vue'
+import { Component } from 'vue-property-decorator'
+import { mapActions, mapState, mapGetters } from 'vuex'
+import { handleSuccessAsync, handleError, ArrayFlat, transToGmtTime } from '../../../../../../util'
+import { pageRefTags, NamedRegex1, NamedRegex, pageCount } from '../../../../../../config'
+import filterElements from '../../../../../../filter/index'
+
+@Component({
+  props: {
+    modelDesc: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    }
+  },
+  computed: {
+    ...mapState({
+      currentProject: state => state.project.selected_project
+    }),
+    ...mapGetters([
+      'datasourceActions'
+    ])
+  },
+  methods: {
+    ...mapActions({
+      getAllRecommendations: 'GET_ALL_RECOMMENDATIONS',
+      deleteRecommendations: 'DELETE_RECOMMENDATIONS',
+      accessRecommendations: 'ACCESS_RECOMMENDATIONS',
+      getRecommendDetails: 'GET_RECOMMEND_DETAILS',
+      validateRecommend: 'VALIDATE_RECOMMEND',
+      refreshRecommendationCount: 'RECOMMENDATION_COUNT_REFRESH'
+    }),
+    ...mapActions('ConfirmSegment', {
+      callConfirmSegmentModal: 'CALL_MODAL'
+    })
+  },
+  locales: {
+    en: {
+      recommendations: 'Recommendations',
+      recommendationsTip1: 'Recommendations are generated by analyzing the query history and model usage.',
+      recommendationsTip2: 'in project settings.',
+      modifyRules: ' Modify rules ',
+      th_recommendType: 'Type',
+      th_name: 'Name',
+      th_table: 'Table',
+      th_column: 'Column',
+      th_dataType: 'Data Type',
+      th_function: 'Function',
+      th_parameter: 'Function Parameter',
+      th_expression: 'Expression',
+      th_dataSize: 'Data Size',
+      th_useCount: 'Usage',
+      th_column_count: 'Column Totals',
+      th_updateDate: 'Last Updated Time',
+      th_note: 'Note',
+      th_source: 'Source',
+      th_type: 'Type',
+      imported: 'Import',
+      query_history: 'Query History',
+      AGG: 'Aggregate Index',
+      TABLE: 'Table Index',
+      ADD_AGG_INDEX: 'Add Aggregate Index',
+      REMOVE_AGG_INDEX: 'Delete Aggregate Index',
+      ADD_TABLE_INDEX: 'Add Table Index',
+      REMOVE_TABLE_INDEX: 'Delete Table Index',
+      usage_time_tip: 'The usage of this index in the past {date} is lower than {time} times.',
+      exist_index_tip: 'There already exists one index or more who could include this index.',
+      similar_index_tip: 'There already exists one index or more who has a high similarity with this index.',
+      imported_index_tip: 'Generated by the imported SQLs',
+      query_history_tip: 'Generated by query history',
+      LOW_FREQUENCY: 'Low Frequency',
+      INCLUDED: 'Included by other indexes',
+      SIMILAR: 'High Similarity',
+      IMPORTED: 'Imported',
+      QUERY_HISTORY: 'Query History',
+      accept: 'Accept',
+      delete: 'Delete',
+      usedCountTip: 'For adding indexes, it means how many historical queries could be optimized;  for deleting indexes, it means how many times the index has been used.',
+      viewDetail: 'Details',
+      deleteRecommendTip: 'Selected recommendations would be permanently deleted. Do you want to continue?',
+      deleteTitle: 'Delete Recommendations',
+      deleteSuccess: 'Deleted successfully',
+      aggDetailTitle: 'Aggregate Index Details',
+      tableDetailTitle: 'Table Index Details',
+      order: 'Order',
+      cc: 'Computed Columns',
+      dimension: 'Dimension',
+      measure: 'Measure',
+      newAdd: 'Add',
+      cardinality: 'Cardinality',
+      cardinalityColumnTips: 'Total amount of unique data in this column. Could be gathered from sampling.',
+      validateTitle: 'Add Items to Model',
+      validateModalTip: 'To accept the selected recommendations, the following items have to be added to the model:',
+      add: 'Add',
+      requiredName: 'Please enter alias',
+      sameNameTips: 'The name already exists. Please rename and try again.',
+      bothAcceptAddAndDelete: 'Successfully added {addLength} index(es), and deleted {delLength} index(es)',
+      onlyAcceptAdd: 'Successfully added {addLength} index(es)',
+      onlyAcceptDelete: 'Successfully deleted {delLength} index(es)',
+      bothAddAggAndTableBaseIndex: '{createBaseIndexNum} base index(es) would be added, {updatebaseIndexNum} base index(es) would be updated',
+      onlyCreateBaseIndex: '{createBaseIndexNum} base index(es) would be added',
+      onlyUpdateBaseIndex: '{updatebaseIndexNum} base index(es) would be updated',
+      buildIndexTip: ' Build Index',
+      buildIndex: 'Build Index',
+      batchBuildSubTitle: 'Please choose which data ranges you\'d like to build with the added indexes.',
+      onlyStartLetters: 'Only supports starting with a letter',
+      usedInOtherModel: 'Can\'t rename this computed column, as it\'s been used in other models.',
+      searchContentOrIndexId: 'Search index content or ID',
+      indexContent: 'Content',
+      viewIndexDetails: 'More details',
+      viewAll: 'View all',
+      indexesContent: 'Index Content',
+      realTimeModelAcceptRecommendTips: 'To accept recommendations for streaming indexes, follow the steps: stop the streaming job, delete all the streaming segments, accept recommendations, start the streaming job.'
+    },
+    'zh-cn': {
+      recommendations: '优化建议',
+      recommendationsTip1: '以下为系统根据您的查询历史及使用情况对模型生成的优化建议。',
+      recommendationsTip2: '可在项目设置中',
+      modifyRules: '配置规则',
+      th_recommendType: '建议类型',
+      th_name: '名称',
+      th_table: '表',
+      th_column: '列',
+      th_dataType: '数据类型',
+      th_function: '函数',
+      th_parameter: '函数参数',
+      th_expression: '表达式',
+      th_dataSize: '数据大小',
+      th_useCount: '使用次数',
+      th_column_count: '列数总计',
+      th_updateDate: '最后更新时间',
+      th_note: '备注',
+      th_source: '来源',
+      th_type: '类型',
+      imported: '导入',
+      query_history: '查询历史',
+      AGG: '聚合索引',
+      TABLE: '明细索引',
+      ADD_AGG_INDEX: '新增聚合索引',
+      REMOVE_AGG_INDEX: '删除聚合索引',
+      ADD_TABLE_INDEX: '新增明细索引',
+      REMOVE_TABLE_INDEX: '删除明细索引',
+      usage_time_tip: '该索引在过去{date}内使用频率低于{time}次。',
+      exist_index_tip: '已有索引可以包含该索引。',
+      similar_index_tip: '存在与该索引相似的索引。',
+      imported_index_tip: '根据导入 SQL 生成',
+      query_history_tip: '根据查询历史生成',
+      LOW_FREQUENCY: '低频使用',
+      INCLUDED: '包含关系',
+      SIMILAR: '高相似度',
+      IMPORTED: '导入',
+      QUERY_HISTORY: '查询历史',
+      accept: '通过',
+      delete: '删除',
+      usedCountTip: '若为新增索引,表示该索引可优化多少条历史查询;若为删除索引,表示该索引被使用的次数。',
+      viewDetail: '查看详情',
+      deleteRecommendTip: '所选优化建议删除后不可恢复。确定要删除吗?',
+      deleteTitle: '删除优化建议',
+      deleteSuccess: '已删除',
+      aggDetailTitle: '聚合索引详情',
+      tableDetailTitle: '明细索引详情',
+      order: '顺序',
+      cc: '可计算列',
+      dimension: '维度',
+      measure: '度量',
+      newAdd: '新增',
+      cardinality: '基数',
+      cardinalityColumnTips: '该列不重复的数据量。该数据可通过采样获得。',
+      validateTitle: '添加以下内容至模型',
+      validateModalTip: '通过所选优化建议需要添加以下内容至模型:',
+      add: '确认添加',
+      requiredName: '请输入别名',
+      sameNameTips: '该名称已存在,请重新命名。',
+      bothAcceptAddAndDelete: '成功新增 {addLength} 条索引,删除 {delLength} 条索引',
+      onlyAcceptAdd: '成功新增 {addLength} 条索引',
+      onlyAcceptDelete: '成功删除 {delLength} 条索引',
+      bothAddAggAndTableBaseIndex: '新增 {createBaseIndexNum} 个基础索引,更新 {updatebaseIndexNum} 个基础索引',
+      onlyCreateBaseIndex: '新增 {createBaseIndexNum} 个基础索引',
+      onlyUpdateBaseIndex: '更新 {updatebaseIndexNum} 个基础索引',
+      buildIndexTip: '立即构建索引',
+      buildIndex: '构建索引',
+      batchBuildSubTitle: '请为新增的索引选择需要构建至的数据范围。',
+      onlyStartLetters: '仅支持字母开头',
+      usedInOtherModel: '该可计算列已在其他模型中使用,不可修改名称。',
+      searchContentOrIndexId: '搜索索引内容或 ID',
+      indexContent: '内容',
+      viewIndexDetails: '更多详情',
+      viewAll: '查看全部',
+      indexesContent: '索引内容',
+      realTimeModelAcceptRecommendTips: '通过实时索引的优化建议需依次进行以下操作:停止实时任务,清空实时 Segment,通过优化建议,启动实时任务。'
+    }
+  }
+})
+export default class IndexList extends Vue {
+  pageRefTags = pageRefTags
+  transToGmtTime = transToGmtTime
+  loadingRecommends = false
+  loadingDetails = false
+  recommendationsList = {
+    list: [],
+    page_offset: 0,
+    totalSize: 0,
+    sort_by: '',
+    reverse: false,
+    key: '',
+    page_size: +localStorage.getItem(this.pageRefTags.recommendationsPager) || pageCount
+  }
+
+  typeList = ['ADD_AGG_INDEX', 'REMOVE_AGG_INDEX', 'ADD_TABLE_INDEX', 'REMOVE_TABLE_INDEX']
+  source = ['imported', 'query_history']
+  lowFrequency = {
+    frequency_time_window: '',
+    low_frequency_threshold: 0
+  }
+
+  checkedStatus = []
+  sourceCheckedStatus = []
+  selectedList = []
+  showIndexDetail = false
+  detailData = []
+  currentIndex = null
+  accessLoading = false
+  validateData = {}
+  showValidate = false
+  isLoading = false
+  rules = {
+    name: [{ required: true, validator: this.validateName, trigger: 'blur' }]
+  }
+
+  rulesCC = {
+    name: [{ required: true, validator: this.validateNameCC, trigger: 'blur' }]
+  }
+
+  hasError = false
+  sameNameErrorIds = []
+
+  get emptyText () {
+    return this.$t('kylinLang.common.noData')
+  }
+
+  get indexDetailTitle () {
+    return this.currentIndex ? this.currentIndex.type.split('_')[1] === 'AGG' ? this.$t('aggDetailTitle') : this.$t('tableDetailTitle') : ''
+  }
+
+  get getValidateList () {
+    return this.validateData.list.filter(item => (item.type === 'dimension' && item.add) || (item.type === 'measure' && item.add) || (item.type === 'cc' && item.add))
+  }
+
+  sameNameErrorStatus (id) {
+    return this.sameNameErrorIds.includes(id)
+  }
+
+  // 更改 input 内容
+  changeInputContent () {
+    this.sameNameErrorIds = []
+  }
+
+  formatDataSize (dataSize) {
+    if (dataSize < 0) {
+      return ''
+    } else {
+      return filterElements.dataSize(dataSize)
+    }
+  }
+
+  getCellClassName (scope) {
+    return scope.columnIndex === 0 && !scope.row.content ? 'hide-cell-expand' : ''
+  }
+
+  created () {
+    this.getRecommendations()
+  }
+
+  mounted () {
+  }
+
+  validateName (rule, value, callback) {
+    if (!value || !value.trim()) {
+      callback(new Error(this.$t('requiredName')))
+    } else if (!NamedRegex1.test(value)) {
+      callback(new Error(this.$t('kylinLang.common.nameFormatValidTip2')))
+    } else {
+      callback()
+    }
+  }
+
+  validateNameCC (rule, value, callback) {
+    if (!NamedRegex.test(value.toUpperCase())) {
+      return callback(new Error(this.$t('kylinLang.common.nameFormatValidTip')))
+    } else if (/^\d|^_+/.test(value)) {
+      return callback(new Error(this.$t('onlyStartLetters')))
+    } else {
+      callback()
+    }
+  }
+
+  async acceptLayout () {
+    this.showIndexDetail = false
+    await this.doConfirm([this.currentIndex])
+  }
+
+  handleSelectionChange (val) {
+    this.selectedList = val
+  }
+
+  // 展示优化建议详情
+  showDetail (row) {
+    this.currentIndex = row
+    this.showIndexDetail = true
+    this.detailData = [...row.rec_detail_response.cc_items.map(it => ({ ...it, type: 'cc' })), ...row.rec_detail_response.dimension_items.map(it => ({ ...it, type: 'dimension' })), ...row.rec_detail_response.measure_items.map(it => ({ ...it, type: 'measure' }))]
+  }
+
+  // 获取优化建议
+  getRecommendations (type) {
+    const { page_offset, page_size, reverse, sort_by, key } = this.recommendationsList
+    this.loadingRecommends = true
+    this.getAllRecommendations({
+      project: this.currentProject,
+      modelId: this.modelDesc.uuid,
+      page_offset,
+      page_size,
+      type: this.checkedStatus.join(','),
+      reverse,
+      key,
+      sort_by
+    }).then(async (res) => {
+      const data = await handleSuccessAsync(res)
+      this.recommendationsList.list = data.layouts
+      this.recommendationsList.totalSize = data.size
+      if (type && type === 'refreshCount') {
+        this.refreshModelRecCount()
+      }
+      this.loadingRecommends = false
+      if (data.broken_recs && data.broken_recs.length > 0) {
+        this.refreshRecommendationCount({ model_id: this.modelDesc.uuid, project: this.currentProject, action: 'refresh' })
+      }
+    }).catch(e => {
+      handleError(e)
+      this.loadingRecommends = false
+    })
+  }
+
+  getRowContentList (row) {
+    return [...row.rec_detail_response.dimension_items, ...row.rec_detail_response.measure_items, ...row.rec_detail_response.cc_items]
+  }
+
+  // 批量删除
+  batchDelete () {
+    const recs_to_add_layout = []
+    const recs_to_remove_layout = []
+    this.selectedList.forEach(item => {
+      if (item.type.split('_')[0] === 'ADD') {
+        recs_to_add_layout.push(item.item_id)
+      } else {
+        recs_to_remove_layout.push(item.item_id)
+      }
+    })
+    this.removeApi(recs_to_add_layout, recs_to_remove_layout)
+  }
+
+  // 删除优化建议
+  removeIndex (row) {
+    const recs_to_add_layout = []
+    const recs_to_remove_layout = []
+    row.type.split('_')[0] === 'ADD' ? recs_to_add_layout.push(row.item_id) : recs_to_remove_layout.push(row.item_id)
+    this.removeApi(recs_to_add_layout, recs_to_remove_layout)
+  }
+
+  async removeApi (recs_to_add_layout, recs_to_remove_layout) {
+    await this.$confirm(this.$t('deleteRecommendTip'), this.$t('deleteTitle'), {
+      confirmButtonText: this.$t('delete')
+    })
+    this.deleteRecommendations({
+      project: this.currentProject,
+      modelId: this.modelDesc.uuid,
+      recs_to_add_layout: recs_to_add_layout.join(','),
+      recs_to_remove_layout: recs_to_remove_layout.join(',')
+    }).then(async (res) => {
+      await handleSuccessAsync(res)
+      this.$message({
+        type: 'success',
+        message: this.$t('deleteSuccess')
+      })
+      this.recommendationsList.page_offset = 0
+      this.getRecommendations('refreshCount')
+    }).catch(e => {
+      handleError(e)
+    })
+  }
+
+  // 批量通过
+  batchAccept () {
+    this.doConfirm(this.selectedList)
+  }
+
+  // 通过优化建议
+  doConfirm (idList) {
+    this.validateRecommend({
+      project: this.currentProject,
+      modelId: this.modelDesc.uuid,
+      recs_to_add_layout: idList.filter(it => it.is_add).map(v => v.item_id),
+      recs_to_remove_layout: idList.filter(it => !it.is_add).map(v => v.item_id)
+    }).then(async (res) => {
+      const data = await handleSuccessAsync(res)
+      const { recs_to_add_layout, recs_to_remove_layout, cc_items, dimension_items, measure_items } = data
+      const addCCLen = cc_items.filter(it => it.add).length
+      const addDimensionLen = dimension_items.filter(it => it.add).length
+      const addMeasure = measure_items.filter(it => it.add).length
+      if (recs_to_add_layout.length && addCCLen + addDimensionLen + addMeasure > 0) {
+        this.showValidate = true
+        this.validateData = {
+          recs_to_add_layout,
+          recs_to_remove_layout,
+          list: [
+            ...cc_items.map(it => ({ ...it, name: it.name.split('.').splice(-1).join(''), type: 'cc' })),
+            ...dimension_items.map(it => {
+              const name = it.name.split('.').splice(-1).join('')
+              if (this.sameNameValidation(name).length > 1) {
+                return { ...it, name: it.name.split('.').reverse().join('_'), type: 'dimension' }
+              } else {
+                return { ...it, name: it.name.split('.').splice(-1).join(''), type: 'dimension' }
+              }
+            }),
+            ...measure_items.filter(item => item.name !== 'COUNT_ALL').map(it => ({ ...it, type: 'measure' }))
+          ]
+        }
+      } else {
+        this.validateData = { recs_to_add_layout, recs_to_remove_layout, list: [] }
+        this.accessApi(recs_to_add_layout, recs_to_remove_layout)
+      }
+    }).catch((e) => {
+      handleError(e)
+    })
+  }
+
+  // 判断是否同名,同名则改成 column_table 形式
+  sameNameValidation (name) {
+    const { all_named_columns } = this.modelDesc
+    return all_named_columns.filter(v => {
+      const table = v.column ? v.column.split('.')[0] : ''
+      const regx = new RegExp(`_${table}$`)
+      return v.name === name || v.name.replace(regx, '') === name
+    })
+  }
+
+  // 添加更名后的layout
+  async addLayout () {
+    const rfs = Object.keys(this.$refs).filter(it => /^validateForm_/.test(it) && this.$refs[it] && this.$refs[it].model.add)
+    for (const item of rfs) {
+      const flag = await this.$refs[item].validate()
+      if (!flag) {
+        this.hasError = true
+        break
+      }
+    }
+    if (this.hasError) return
+    const names = {}
+    const { recs_to_add_layout, recs_to_remove_layout } = this.validateData
+    this.isLoading = true
+    this.validateData.list.forEach((it) => {
+      names[it.item_id] = it.name
+    })
+    this.accessApi(recs_to_add_layout, recs_to_remove_layout, names).then(() => {
+      this.sameNameErrorIds = []
+      this.isLoading = false
+      this.showValidate = false
+    }).catch(e => {
+      this.isLoading = false
+    })
+  }
+
+  // 建议通过接口调用
+  async accessApi (recs_to_add_layout, recs_to_remove_layout, names) {
+    names = names || {}
+    return new Promise((resolve, reject) => {
+      const emptyName = Object.values(names).some(it => !it)
+      if (emptyName) {
+        reject(new Error('emptyName'))
+      } else {
+        this.accessRecommendations({
+          project: this.currentProject,
+          modelId: this.modelDesc.uuid,
+          recs_to_add_layout,
+          recs_to_remove_layout,
+          names
+        }).then(async (res) => {
+          try {
+            const result = await handleSuccessAsync(res)
+            const acceptIndexs = () => {
+              return {
+                add: result.added_layouts.length,
+                del: result.removed_layouts.length
+              }
+            }
+            this.$message({
+              type: 'success',
+              message: <span>{this.getAcceptIndexes(acceptIndexs) + this.acceptBaseIndex(result)}<a href="javascript:void(0);" onClick={() => this.buildIndex({ layoutIds: [...result.added_layouts, ...this.getBaseIndexLayout(result)] })}>{
+                this.canBuildIndex(acceptIndexs, result) ? this.$t('buildIndex') : ''
+              }</a></span>
+            })
+            this.recommendationsList.page_offset = 0
+            this.getRecommendations('refreshCount')
+            // this.$emit('accept')
+            this.$emit('refreshModel')
+            resolve()
+          } catch (e) {
+            reject(e)
+          }
+        }).catch(error => {
+          const { body: { exception } } = error
+          try {
+            const data = JSON.parse(exception.split('\n')[1])
+            const errorIds = ArrayFlat(Object.values(data))
+            if (errorIds.length > 0) {
+              this.sameNameErrorIds = errorIds
+            } else {
+              handleError(error)
+            }
+          } catch (e) {
+            handleError(error)
+          }
+          // handleError(e)
+          reject(error)
+        })
+      }
+    })
+  }
+
+  // 是否需要展示构建索引内容
+  canBuildIndex (acceptIndexs, result) {
+    const { createBaseIndexNum, updateBaseIndexNum } = this.getBaseIndexCount(result.base_index_info)
+    return ((acceptIndexs().add > 0 && acceptIndexs().del > 0) || acceptIndexs().add > 0 || createBaseIndexNum > 0 || updateBaseIndexNum > 0) && this.modelDesc.segments.length && this.modelDesc.model_type !== 'STREAMING'
+  }
+
+  // 获取 base index layoutId
+  getBaseIndexLayout (result) {
+    const { base_agg_index, base_table_index } = result.base_index_info
+    const list = []
+    base_agg_index && list.push(base_agg_index.layout_id)
+    base_table_index && list.push(base_table_index.layout_id)
+    return list
+  }
+
+  // 获取 base index 创建或更新的数量
+  getBaseIndexCount (result) {
+    const { base_agg_index, base_table_index } = result
+    const createBaseIndexNum = [...[base_agg_index ?? {}].filter(it => it.operate_type === 'CREATE'), ...[base_table_index ?? {}].filter(it => it.operate_type === 'CREATE')].length
+    const updateBaseIndexNum = [...[base_agg_index ?? {}].filter(it => it.operate_type === 'UPDATE'), ...[base_table_index ?? {}].filter(it => it.operate_type === 'UPDATE')].length
+    return { createBaseIndexNum, updateBaseIndexNum }
+  }
+
+  // 是否有接受或删除的 layout
+  getAcceptIndexes (acceptIndexsFunc) {
+    const acceptIndexs = (() => acceptIndexsFunc())()
+    return acceptIndexs.add > 0 && acceptIndexs.del > 0
+      ? this.$t('bothAcceptAddAndDelete', { addLength: acceptIndexs.add, delLength: acceptIndexs.del })
+      : acceptIndexs.add > 0
+        ? this.$t('onlyAcceptAdd', { addLength: acceptIndexs.add })
+        : this.$t('onlyAcceptDelete', { delLength: acceptIndexs.del })
+  }
+
+  // 是否有创建或刷新的基础索引
+  acceptBaseIndex (res) {
+    const { createBaseIndexNum, updateBaseIndexNum } = this.getBaseIndexCount(res.base_index_info)
+    return createBaseIndexNum > 0 && updateBaseIndexNum > 0
+      ? `${this.$t('kylinLang.common.comma')}${this.$t('bothAddAggAndTableBaseIndex', { createBaseIndexNum, updatebaseIndexNum: updateBaseIndexNum })}${this.$t('kylinLang.common.dot')}`
+      : createBaseIndexNum > 0
+        ? `${this.$t('kylinLang.common.comma')}${this.$t('onlyCreateBaseIndex', { createBaseIndexNum })}${this.$t('kylinLang.common.dot')}`
+        : updateBaseIndexNum > 0
+          ? `${this.$t('kylinLang.common.comma')}${this.$t('onlyUpdateBaseIndex', { updatebaseIndexNum: updateBaseIndexNum })}${this.$t('kylinLang.common.dot')}`
+          : `${this.$t('kylinLang.common.dot')}`
+  }
+
+  // 新增建议构建索引
+  buildIndex ({ layoutIds }) {
+    this.callConfirmSegmentModal({
+      title: this.$t('buildIndex'),
+      subTitle: this.$t('batchBuildSubTitle'),
+      indexes: layoutIds || [],
+      submitText: this.$t('buildIndex'),
+      model: this.modelDesc
+    })
+  }
+
+  // 删除索引备注hover提示
+  removeReasonTip (data) {
+    const timeMap = {
+      MONTH: { 'zh-cn': '一个月', en: 'month' },
+      DAY: { 'zh-cn': '一天', en: 'day' },
+      WEEK: { 'zh-cn': '一周', en: 'week' }
+    }
+    const reason = {
+      LOW_FREQUENCY: this.$t('usage_time_tip', { date: this.lowFrequency.frequency_time_window && timeMap[this.lowFrequency.frequency_time_window][this.$store.state.system.lang], time: this.lowFrequency.low_frequency_threshold }),
+      INCLUDED: this.$t('exist_index_tip'),
+      SIMILAR: this.$t('similar_index_tip'),
+      IMPORTED: this.$t('imported_index_tip'),
+      QUERY_HISTORY: this.$t('query_history_tip')
+    }
+    return reason[data.row.memo_info.recommendation_source]
+  }
+
+  // 筛选类型来源
+  filterType (v, type) {
+    this.recommendationsList.page_offset = 0
+    type === 'checkedStatus' ? (this.checkedStatus = v) : (this.sourceCheckedStatus = v)
+    this.getRecommendations()
+  }
+
+  // 分页操作
+  pageCurrentChange (offset, size) {
+    if (size !== this.recommendationsList.page_size) {
+      this.recommendationsList.page_offset = 0
+      this.recommendationsList.page_size = size
+    } else {
+      this.recommendationsList.page_offset = offset
+    }
+    this.getRecommendations()
+  }
+
+  // 更改排序
+  changeSort ({ prop, order }) {
+    this.recommendationsList = {
+      ...this.recommendationsList,
+      reverse: order === 'descending',
+      sort_by: prop,
+      page_offset: 0
+    }
+    this.getRecommendations()
+  }
+
+  // 跳转至 setting 设置界面
+  jumpToSetting () {
+    this.$router.push({ path: '/setting', query: { moveTo: 'index-suggest-setting' } })
+  }
+
+  // 刷新模型列表上的优化建议个数
+  refreshModelRecCount () {
+    const { page_offset, page_size, reverse, sort_by } = this.recommendationsList
+    this.getAllRecommendations({
+      project: this.currentProject,
+      modelId: this.modelDesc.uuid,
+      page_offset,
+      page_size,
+      type: '',
+      reverse,
+      sort_by
+    }).then(async (res) => {
+      const data = await handleSuccessAsync(res)
+      this.modelDesc.recommendations_count = data.size
+    })
+  }
+
+  // 搜索优化建议
+  searchRecommendation () {
+    this.recommendationsList.page_offset = 0
+    this.getRecommendations()
+  }
+}
+</script>
+
+<style lang="less">
+@import '../../../../../../assets/styles/variables.less';
+.el-card.recommendations-card {
+  border: none;
+  margin-top: 16px;
+  .el-card__header {
+    background: none;
+    border-bottom: none;
+    height: 24px;
+    font-size: 14px;
+    padding: 0px;
+    margin-bottom: 5px;
+  }
+  .el-card__body {
+    padding: 0 !important;
+
+    .detail-content {
+      background-color: transparent;
+    }
+  }
+  .title-tip {
+    color: @text-normal-color;
+    font-size: 12px;
+    font-weight: 400;
+    line-height: 18px;
+  }
+}
+.el-table.index-details-table {
+  .cell {
+    height: 28px;
+    line-height: 28px;
+    span:first-child {
+      line-height: 28px;
+      vertical-align: revert;
+    }
+    .el-table__expand-icon {
+      >.el-icon {
+        margin-top: -2px;
+      }
+    }
+    .column-name {
+      display: inline-block;
+      width: calc(~'100% - 50px');
+      text-overflow: ellipsis;
+      overflow: hidden;
+      white-space: nowrap;
+    }
+  }
+  .expanded {
+    .el-table__expand-icon {
+      >.el-icon {
+        margin-top: -6px;
+        margin-left: -2px;
+      }
+    }
+  }
+  .el-table__expanded-cell {
+    padding: 24px;
+    font-size: 12px;
+    color: @text-title-color;
+    p {
+      margin-bottom: 5px;
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+    .label {
+      color: @text-normal-color;
+    }
+  }
+  .add-tag {
+    position: absolute;
+    right: 10px;
+    top: 8px;
+  }
+  .el-table__expand-column.hide-cell-expand {
+    .cell {
+      pointer-events: none;
+    }
+    .el-table__expand-icon {
+      color: @text-disabled-color;
+      cursor: not-allowed;
+    }
+  }
+}
+.el-table.validate-table {
+  margin-top: 10px;
+  .el-form-item__content {
+    line-height: 23px;
+    > .is-error {
+      .el-input__inner {
+        border: 1px solid @error-color-1;
+      }
+    }
+    .error-text {
+      color: @error-color-1;
+      font-size: 12px;
+    }
+  }
+  .cell {
+    height: initial;
+  }
+  .el-table__expanded-cell {
+    padding: 10px;
+    font-size: 12px;
+    color: @text-title-color;
+    p {
+      margin-bottom: 5px;
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+    .label {
+      color: @text-normal-color;
+    }
+  }
+}
+.layout-details {
+  .el-dialog__body {
+    max-height: 400px;
+    overflow: auto;
+    .el-form-item__error {
+      white-space: normal;
+    }
+  }
+}
+.col-index-content-popover {
+  .popover-header {
+    position: relative;
+    margin-bottom: 8px;
+  }
+  .view-more-btn {
+    position: absolute;
+    right: 0;
+    top: 0;
+    padding: 0;
+    i {
+      font-size: 12px;
+    }
+  }
+}
+.recommendations-table{
+  .detail-icon {
+    position: absolute;
+    top: 50%;
+    transform: translate(0, -44%);
+    right: 15px;
+    font-size: 18px;
+    cursor: pointer;
+  }
+  .index-content {
+    cursor: pointer;
+    width: calc(~'100% - 30px');
+    height: 23px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 1;
+     /*! autoprefixer: off */
+    -webkit-box-orient: vertical;
+    /* autoprefixer: on */
+    white-space: nowrap\0;
+  }
+}
+
+</style>
diff --git a/kystudio/src/components/studio/StudioModel/ModelList/ModelLayout/modelLayout.vue b/kystudio/src/components/studio/StudioModel/ModelList/ModelLayout/modelLayout.vue
index 033a915f9a..5adf70b911 100644
--- a/kystudio/src/components/studio/StudioModel/ModelList/ModelLayout/modelLayout.vue
+++ b/kystudio/src/components/studio/StudioModel/ModelList/ModelLayout/modelLayout.vue
@@ -89,6 +89,9 @@
               @refreshModel="refreshModelData"
               v-if="currentIndexTab === 'indexOverview'" />
           </el-tab-pane>
+          <el-tab-pane class="tab-pane-item" :label="$t('recommendationsBtn')" name="recommendations" v-if="$store.state.project.isSemiAutomatic && (datasourceActions.includes('accelerationActions') || isAdvancedOperatorUser()) && currentModelRow.model_type !== 'HYBRID'">
+            <recommendations v-if="currentIndexTab==='recommendations'" @refreshModel="refreshModelData" :modelDesc="currentModelRow" />
+          </el-tab-pane>
           <el-tab-pane class="tab-pane-item" v-if="datasourceActions.includes('editAggGroup') || isAdvancedOperatorUser()" :label="$t('aggregateGroup')" name="aggGroup">
             <ModelAggregateView
               :model="currentModelRow"
@@ -132,9 +135,9 @@
 import Vue from 'vue'
 import { Component } from 'vue-property-decorator'
 import { mapState, mapGetters, mapActions } from 'vuex'
-import { handleError, jumpToJobs } from 'util/business'
-import { transToServerGmtTime, handleSuccessAsync } from 'util'
-import { pageCount } from 'config'
+import { handleError, jumpToJobs } from '../../../../../util/business'
+import {getQueryString, transToServerGmtTime, handleSuccessAsync } from '../../../../../util'
+import { pageCount } from '../../../../../config'
 import locales from './locales'
 import ModelOverview from '../ModelOverview/ModelOverview.vue'
 import ModelSegment from '../ModelSegment/index.vue'
@@ -153,6 +156,7 @@ import ModelActions from '../ModelActions/modelActions'
 import ModelRenameModal from '../ModelRenameModal/rename.vue'
 import ModelCloneModal from '../ModelCloneModal/clone.vue'
 import ModelPartition from '../ModelPartition/index.vue'
+import Recommendations from '../ModelAggregate/sub/recommendations.vue'
 import ModelTitleDescription from '../Components/ModelTitleDescription'
 
 @Component({
@@ -162,7 +166,7 @@ import ModelTitleDescription from '../Components/ModelTitleDescription'
       if (to.query.modelListFilters || getQueryString('modelListFilters')) {
         vm.modelListFilters = JSON.parse(to.query.modelListFilters) || JSON.parse(getQueryString('modelListFilters'))
       }
-  
+
       if (from.name === 'ModelList') {
         vm.filterData = vm.modelListFilters
       } else if (to.query.filterData) {
@@ -222,6 +226,7 @@ import ModelTitleDescription from '../Components/ModelTitleDescription'
     ModelRenameModal,
     ModelCloneModal,
     ModelPartition,
+    Recommendations,
     ModelTitleDescription
   },
   locales
@@ -334,10 +339,13 @@ export default class ModelLayout extends Vue {
       // 能通过模型名称获取到模型数据,说明该模型存在
       if (value.length) {
         if (type === 'init') {
-          const { tabTypes, createSecStorageIndex, indexTab } = this.$route.params
-          this.showCreateOrEditSecStorageIndex = createSecStorageIndex ?? false
+          const { jump, tabTypes, indexTab } = this.$route.params
           this.currentIndexTab = indexTab ?? 'indexOverview'
-          this.currentModelRow = {tabTypes: typeof tabTypes !== 'undefined' ? tabTypes : 'overview', ...value[0]}
+          const isNeedJumpToRecommendation = (jump && jump === 'recommendation') || (this.platform === 'iframe' && getQueryString('jump') === 'recommendation')
+          this.currentModelRow = { tabTypes: isNeedJumpToRecommendation ? 'second' : (typeof tabTypes !== 'undefined' ? tabTypes : 'overview'), ...value[0] }
+          if (isNeedJumpToRecommendation) {
+            this.jumpToRecommendation()
+          }
           if (this.currentModelRow.tabTypes === 'second' && localStorage.getItem('isFirstSaveModel') === 'true') {
             this.showGuide()
           }
@@ -480,6 +488,14 @@ export default class ModelLayout extends Vue {
     this.$refs['segmentComp' + alias] && await this.$refs['segmentComp' + alias].$emit('willAddIndex')
   }
 
+  // 跳转并展示优化建议界面
+  jumpToRecommendation () {
+    this.$nextTick(() => {
+      this.currentIndexTab = 'recommendations'
+      // this.$refs.modelAggregateItem && (this.$refs.modelAggregateItem.switchIndexValue = 'rec')
+    })
+  }
+
   // 更改模型名称
   changeModelName (name) {
     this.currentModelRow.alias = name
diff --git a/kystudio/src/components/studio/StudioModel/ModelList/index.vue b/kystudio/src/components/studio/StudioModel/ModelList/index.vue
index 7d457f563a..3e49ea446c 100644
--- a/kystudio/src/components/studio/StudioModel/ModelList/index.vue
+++ b/kystudio/src/components/studio/StudioModel/ModelList/index.vue
@@ -7,6 +7,7 @@
       <div class="ksd-fright">
         <div class="ky-no-br-space model-list-header">
           <el-dropdown
+            v-guide.addModelBtn
             split-button
             class="ksd-fleft"
             type="primary"
@@ -17,6 +18,11 @@
             v-if="datasourceActions.includes('modelActions')"
             @click="showAddModelDialog">{{$t('kylinLang.common.new')}}
             <el-dropdown-menu slot="dropdown" class="model-actions-dropdown">
+              <el-dropdown-item
+                v-if="$store.state.project.isSemiAutomatic && datasourceActions.includes('modelActions')"
+                @click="showGenerateModelDialog">
+                {{$t('kylinLang.model.generateModel')}}
+              </el-dropdown-item>
               <el-dropdown-item
                 v-if="metadataActions.includes('executeModelMetadata')"
                 @click="handleImportModels">
@@ -83,7 +89,16 @@
           </el-input>
         </div>
       </div>
+      <p v-if="$store.state.config.platform === 'iframe'" class="ksd-mb-10">
+        <el-alert
+          :title="$t('guideToAcceptRecom')"
+          type="info"
+          show-icon
+          :closable="false">
+        </el-alert>
+      </p>
       <el-table class="model_list_table"
+        v-guide.scrollModelTable
         v-scroll-shadow
         :data="modelArray"
         :empty-text="emptyText"
@@ -107,10 +122,24 @@
           <template slot-scope="scope">
             <model-title-description :modelData="scope.row" @openSegment="openComplementSegment" @autoFix="autoFix" source="modelList" />
             <!-- 工具栏 -->
-            <model-actions :currentModel="scope.row" @loadModelsList="loadModelsList"/>
+            <model-actions :currentModel="scope.row" @loadModelsList="loadModelsList" @jump:recommendation="jumpToRecommendation"/>
 
           </template>
         </el-table-column>
+
+        <el-table-column
+          width="150px"
+          sortable="custom"
+          prop="recommendations_count"
+          :label="$t('recommendationsTiTle')"
+          v-if="$store.state.project.isSemiAutomatic && (datasourceActions.includes('accelerationActions') || isAdvancedOperatorUser())">
+          <template slot-scope="scope">
+            <template v-if="!(scope.row.status !== 'BROKEN' && ('visible' in scope.row && scope.row.visible)) || scope.row.model_type === 'HYBRID'">-</template>
+            <el-tooltip effect="dark" :content="$t('recommendationsTiTle')" placement="bottom" v-else>
+              <el-button type="primary" class="rec-btn" text icon="el-ksd-icon-wizard_22" @click.stop="jumpToRecommendation(scope.row)">{{scope.row.recommendations_count}}</el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
         <el-table-column :label="$t('kylinLang.common.fact')" width="180">
           <template slot-scope="scope">
             <template v-if="scope.row.status === 'BROKEN'">-</template>
@@ -136,6 +165,7 @@
           prop="model_type"
           show-overflow-tooltip
           width="150px"
+          v-if="$store.state.config.platform !== 'iframe'"
           :label="$t('modelType')">
           <template slot-scope="scope">
             <span>{{$t(scope.row.model_type)}}</span>
@@ -147,7 +177,7 @@
           align="right"
           sortable="custom"
           show-overflow-tooltip
-          :info-tooltip="$t('usageTip', {mode: 'model'})"
+          :info-tooltip="$t('usageTip', {mode: $t('model')})"
           :label="$t('usage')">
         </el-table-column>
         <el-table-column
@@ -209,6 +239,8 @@
     <ModelCloneModal/>
     <!-- 模型添加 -->
     <ModelAddModal/>
+    <!-- 推荐模型 -->
+    <UploadSqlModel v-on:reloadModelList="loadModelsList"/>
     <!-- 选择去构建的segment -->
     <ConfirmSegment v-on:reloadModelAndSegment="reloadModelAndSegment"/>
   </div>
@@ -221,8 +253,8 @@ import dayjs from 'dayjs'
 import { NamedRegex, pageRefTags, pageCount } from '../../../../config'
 import { ModelStatusTagType } from '../../../../config/model.js'
 import locales from './locales'
-import { handleError, kylinMessage, jumpToJobs } from 'util/business'
-import { sliceNumber, transToServerGmtTime } from 'util'
+import { handleError, kylinMessage, jumpToJobs } from '../../../../util/business'
+import { handleSuccessAsync, sliceNumber, transToServerGmtTime } from '../../../../util'
 import TableIndex from '../TableIndex/index.vue'
 import ModelSegment from './ModelSegment/index.vue'
 import SegmentTabs from './ModelSegment/SegmentTabs.vue'
@@ -236,6 +268,7 @@ import ConfirmSegment from './ConfirmSegment/ConfirmSegment.vue'
 import ModelPartition from './ModelPartition/index.vue'
 import ModelJson from './ModelJson/modelJson.vue'
 import ModelSql from './ModelSql/ModelSql.vue'
+import UploadSqlModel from '../../../common/UploadSql/UploadSql.vue'
 import { mockSQL } from './mock'
 import DropdownFilter from '../../../common/DropdownFilter/DropdownFilter.vue'
 import ModelOverview from './ModelOverview/ModelOverview.vue'
@@ -308,6 +341,9 @@ function getDefaultFilters (that) {
       streamingEnabled: state => state.system.streamingEnabled
     })
   },
+  inject: [
+    'isAdvancedOperatorUser'
+  ],
   methods: {
     ...mapActions({
       loadModels: 'LOAD_MODEL_LIST',
@@ -317,6 +353,7 @@ function getDefaultFilters (that) {
       fetchSegments: 'FETCH_SEGMENTS',
       downloadModelsMetadata: 'DOWNLOAD_MODELS_METADATA',
       downloadModelsMetadataBlob: 'DOWNLOAD_MODELS_METADATA_BLOB',
+      getFavoriteRules: 'GET_FAVORITE_RULES',
       autoFixSegmentHoles: 'AUTO_FIX_SEGMENT_HOLES'
     }),
     ...mapActions('DetailDialogModal', {
@@ -331,6 +368,12 @@ function getDefaultFilters (that) {
     ...mapActions('ConfirmSegment', {
       callConfirmSegmentModal: 'CALL_MODAL'
     }),
+    ...mapActions('ModelRecommendModal', {
+      callModelRecommendDialog: 'CALL_MODAL'
+    }),
+    ...mapActions('UploadSqlModel', {
+      showUploadSqlDialog: 'CALL_MODAL'
+    }),
     ...mapActions('ModelsImportModal', {
       callModelsImportModal: 'CALL_MODAL'
     }),
@@ -355,13 +398,19 @@ function getDefaultFilters (that) {
     ModelPartition,
     ModelJson,
     ModelSql,
+    UploadSqlModel,
     DropdownFilter,
     ModelOverview,
     ModelActions,
     ModelERDiagram,
     ModelTitleDescription
   },
-  locales
+  locales,
+  provide () {
+    return {
+      getFavoriteRules: this.getFavoriteRulesContext
+    }
+  }
 })
 export default class ModelList extends Vue {
   pageRefTags = pageRefTags
@@ -405,6 +454,12 @@ export default class ModelList extends Vue {
   getER (row) {
     return this.dataGenerator.generateModel(row)
   }
+
+  showGenerateModelDialog () {
+    this.showUploadSqlDialog({
+      isGenerateModel: true
+    })
+  }
   needShowBuildTips (uuid) {
     this.buildVisible[uuid] = !localStorage.getItem('hideBuildTips')
   }
@@ -826,6 +881,17 @@ export default class ModelList extends Vue {
     scrollDom.nextSibling.querySelector('.aggregate-view').scrollIntoView({block: 'center', inline: 'nearest', behavior: 'smooth'})
   }
 
+  async getFavoriteRulesContext () {
+    try {
+      const results = await this.getFavoriteRules({ project: this.currentSelectedProject })
+      const res = await handleSuccessAsync(results)
+      return res
+    } catch (err) {
+      handleError(err)
+      return {}
+    }
+  }
+
   async willAddIndex (alias) {
     this.$refs['segmentComp' + alias] && await this.$refs['segmentComp' + alias].$emit('willAddIndex')
   }
@@ -844,6 +910,10 @@ export default class ModelList extends Vue {
       })
     })
   }
+  // 跳转至指定模型优化建议界面
+  jumpToRecommendation (model) {
+    this.$router.push({ name: 'ModelDetails', params: { modelName: model.alias, jump: 'recommendation' }, query: { modelListFilters: JSON.stringify(this.filterArgs) } })
+  }
   handleAnimateChanged (event) {
     const { className } = event.target
     const isValidClass = className.indexOf('mode-list') !== -1 || className.indexOf('el-loading-mask') !== -1 || className.indexOf('el-button') !== -1
@@ -956,6 +1026,17 @@ export default class ModelList extends Vue {
     }
     .rec-btn {
       color: @ke-color-primary;
+      button-text {
+        min-width: initial;
+      }
+    }
+    .recommendation-layout {
+      display: inline-block;
+      padding: 5px 10px;
+      border: 1px solid @ke-border-secondary;
+      border-radius: 6px;
+      color: @ke-color-primary;
+      cursor: pointer;
     }
     .build-disabled > .el-ksd-icon-build_index_22 {
       color: @color-text-disabled;
@@ -1161,6 +1242,33 @@ export default class ModelList extends Vue {
       cursor: default;
     }
   }
+  .recommend {
+    font-size: 12px;
+    line-height: 18px;
+    float: left;
+    color: @color-primary;
+    i {
+      color: @text-disabled-color;
+      cursor: default;
+    }
+  }
+  .recommend-count {
+    height: 18px;
+    border-radius: 4px;
+    background-color: @base-color-4;
+    color: @fff;
+    padding: 1px 5px;
+    line-height: 16px;
+    font-weight: bold;
+    margin-left: 2px;
+    cursor: pointer;
+
+    b {
+      position: relative;
+      transform: scale(0.833333);
+      display: inline-block;
+    }
+  }
   .streaming {
     float: left;
     font-size: 12px;
@@ -1258,6 +1366,17 @@ export default class ModelList extends Vue {
     }
   }
 }
+.recommend-tooltip {
+  min-width: unset;
+  transform: translate(-5px, 5px);
+  .popper__arrow {
+    left: 5px !important;
+  }
+  .recommend-link {
+    color: @color-primary;
+    cursor: pointer;
+  }
+}
 .model-actions-dropdown {
   text-align: left;
   min-width: 95px;
diff --git a/kystudio/src/config/index.js b/kystudio/src/config/index.js
index 7a986273a4..038d28aa61 100644
--- a/kystudio/src/config/index.js
+++ b/kystudio/src/config/index.js
@@ -159,6 +159,7 @@ export const pageRefTags = {
   tableIndexPager: 'tableIndexPager',
   tableColumnsPager: 'tableColumnsPager',
   statisticsPager: 'statisticsPager',
+  recommendationsPager: 'recommendationsPager',
   subPartitionValuesPager: 'subPartitionValuesPager',
   subPartitionSegmentPager: 'subPartitionSegmentPager',
   snapshotPager: 'snapshotPager',
diff --git a/kystudio/src/config/spec.js b/kystudio/src/config/spec.js
index 0bf9717b25..e20c322b09 100644
--- a/kystudio/src/config/spec.js
+++ b/kystudio/src/config/spec.js
@@ -68,6 +68,7 @@ export default {
       { "id": "acceRuleSettingActions" },
       { "id": "acceDelSql" },
       { "id": "acceBlackList" },
+      { "id": "accelerationActions" },
       { "id": "modelActions" },
       { "id": "addSnapshot" },
       { "id": "snapshotAction" },
@@ -186,9 +187,9 @@ export default {
     "datasourceActions": {
       "keyPattern": "groupRole-projectRole",
       "entries": [
-        { "key": "systemAdmin-*", "value": "sourceManagement,loadSource,delSourceTable,sampleSourceTable,reloadSourceTable,loadData,acceRuleSettingActions,acceBlackList,acceDelSql,modelActions,addSnapshot,snapshotAction,segmentActions,editAggGroup,delAggIdx,buildIndex,tableIndexActions,changePartition,changeBuildType" },
-        { "key": "systemUser-admin", "value": "sourceManagement,loadSource,delSourceTable,sampleSourceTable,reloadSourceTable,loadData,acceRuleSettingActions,acceBlackList,acceDelSql,modelActions,addSnapshot,snapshotAction,segmentActions,editAggGroup,delAggIdx,buildIndex,tableIndexActions,changePartition,changeBuildType" },
-        { "key": "systemUser-management", "value": "loadSource,delSourceTable,sampleSourceTable,reloadSourceTable,loadData,modelActions,addSnapshot,snapshotAction,segmentActions,editAggGroup,delAggIdx,buildIndex,tableIndexActions,changePartition,changeBuildType" },
+        { "key": "systemAdmin-*", "value": "sourceManagement,loadSource,delSourceTable,sampleSourceTable,reloadSourceTable,loadData,acceRuleSettingActions,acceBlackList,acceDelSql,accelerationActions,modelActions,addSnapshot,snapshotAction,segmentActions,editAggGroup,delAggIdx,buildIndex,tableIndexActions,changePartition,changeBuildType" },
+        { "key": "systemUser-admin", "value": "sourceManagement,loadSource,delSourceTable,sampleSourceTable,reloadSourceTable,loadData,acceRuleSettingActions,acceBlackList,acceDelSql,accelerationActions,modelActions,addSnapshot,snapshotAction,segmentActions,editAggGroup,delAggIdx,buildIndex,tableIndexActions,changePartition,changeBuildType" },
+        { "key": "systemUser-management", "value": "loadSource,delSourceTable,sampleSourceTable,reloadSourceTable,loadData,accelerationActions,modelActions,addSnapshot,snapshotAction,segmentActions,editAggGroup,delAggIdx,buildIndex,tableIndexActions,changePartition,changeBuildType" },
         { "key": "systemUser-operation", "value": "loadData,snapshotAction,segmentActions,buildIndex" },
         { "key": "systemUser-read", "value": "none" }
       ]
diff --git a/kystudio/src/directive/index.js b/kystudio/src/directive/index.js
index f66286e96d..6653ba7a46 100644
--- a/kystudio/src/directive/index.js
+++ b/kystudio/src/directive/index.js
@@ -512,6 +512,48 @@ Vue.directive('drag', {
     }
   }
 })
+// 收集guide dom
+Vue.directive('guide', {
+  // bind: function (el, binding, vnode) {
+  //   console.log('----', vnode.key, vnode)
+  //   // 设置alone参数 避免虚拟dom重用导致指令生命周期错误
+  //   let keys = binding.modifiers
+  //   if (binding.arg === 'alone') {
+  //     vnode.key = Object.keys(keys).join('-')
+  //   }
+  // },
+  update: function (el, binding, vnode) {
+    const keys = binding.modifiers
+    const storeGuide = store.state.system.guideConfig.targetList
+    if (storeGuide) {
+      for (const i in keys) {
+        storeGuide[i] = el.__vue__ || el
+      }
+      if (store.state.system.guideConfig.globalMaskVisible && binding.value) {
+        storeGuide[binding.value] = el.__vue__ || el
+      }
+    }
+  },
+  inserted: function (el, binding, vnode) {
+    const keys = binding.modifiers
+    const storeGuide = store.state.system.guideConfig.targetList
+    if (storeGuide) {
+      for (const i in keys) {
+        storeGuide[i] = el.__vue__ || el
+      }
+      if (store.state.system.guideConfig.globalMaskVisible && binding.value) {
+        storeGuide[binding.value] = el.__vue__ || el
+      }
+    }
+  },
+  unbind: function (el, binding) {
+    const keys = binding.modifiers
+    const storeGuide = store.state.system.guideConfig.targetList
+    for (const i in keys) {
+      delete storeGuide[i]
+    }
+  }
+})
 let keyCodeMap = {
   'esc': 27,
   'enter': 13
diff --git a/kystudio/src/locale/en.js b/kystudio/src/locale/en.js
index 1a9a4f48dd..fd1b39f934 100644
--- a/kystudio/src/locale/en.js
+++ b/kystudio/src/locale/en.js
@@ -236,6 +236,7 @@ exports.default = {
     formatTips: 'It is recommended to copying the SQL using the default format. The current format may cause query errors.'
   },
   model: {
+    generateModel: 'From SQL',
     draft: 'Draft',
     modelName: 'Model Name',
     computedColumn: 'Computed Column',
diff --git a/kystudio/src/service/model.js b/kystudio/src/service/model.js
index 7e31580d3b..cf8281bd59 100644
--- a/kystudio/src/service/model.js
+++ b/kystudio/src/service/model.js
@@ -211,12 +211,33 @@ export default {
   complementBatchIndex: (para) => {
     return Vue.resource(apiUrl + `models/${para.modelId}/model_segments/indexes`).save(para.data)
   },
+  getModelRecommendations: (para) => {
+    return Vue.resource(apiUrl + 'models/' + para.model + '/recommendations?project=' + para.project).get()
+  },
+  adoptModelRecommendations: (para) => {
+    return Vue.resource(apiUrl + 'models/' + para.model + '/recommendations').update(para)
+  },
+  clearModelRecommendations: (para) => {
+    return Vue.resource(apiUrl + 'models/' + para.model + '/recommendations').delete(para)
+  },
+  getAggIndexContentList: (para) => {
+    return Vue.resource(apiUrl + 'models/' + para.model + '/recommendations/agg_index').get(para)
+  },
+  getTableIndexContentList: (para) => {
+    return Vue.resource(apiUrl + 'models/' + para.model + '/recommendations/table_index').get(para)
+  },
+  getIndexContentList: (para) => {
+    return Vue.resource(apiUrl + 'models/' + para.model + '/recommendations/index').get(para)
+  },
   deleteBatchIndex: (para) => {
     return Vue.resource(apiUrl + `models/${para.modelId}/model_segments/indexes/deletion`).save(para.data)
   },
   suggestModel: (para) => {
     return Vue.resource(apiUrl + 'models/suggest_model').save(para)
   },
+  saveSuggestModels: (para) => {
+    return Vue.resource(apiUrl + `models/model_recommendation?project=${para.project}`).save(para)
+  },
   validateModelName: (para) => {
     return Vue.resource(apiUrl + 'models/validate_model').save(para)
   },
@@ -292,6 +313,29 @@ export default {
     const responseType = 'blob' || 'arraybuffer'
     return window.kylinVm.$http.post(`/kylin/api/metastore/backup/models?project=${para.project}`, body, { emulateJSON: true }, { headers }, { responseType })
   },
+  // 获取所有优化建议
+  getRecommendations (para) {
+    return Vue.resource(apiUrl + `recommendations/${para.modelId}`).get(para)
+  },
+  // 刷新优化建议
+  recommendationCountRefresh (para) {
+    return Vue.http.put(apiUrl + 'recommendations/count', para)
+  },
+  // 删除优化建议
+  deleteRecommendations (para) {
+    return Vue.resource(apiUrl + `recommendations/${para.modelId}`).delete(para)
+  },
+  // 通过优化建议
+  acceptRecommendations (para) {
+    return window.kapVm.$http.post(apiUrl + `recommendations/${para.modelId}`, para)
+  },
+  // 获取优化建议详情
+  getRecommendDetails (para) {
+    return Vue.resource(apiUrl + `recommendations/${para.modelId}/${para.id}`).get(para)
+  },
+  validateRecommend (para) {
+    return window.kapVm.$http.post(apiUrl + `recommendations/${para.modelId}/validation`, para)
+  },
   fetchHitModelsList (para) {
     return Vue.resource(apiUrl + 'query/query_history_models').get(para)
   },
diff --git a/kystudio/src/service/project.js b/kystudio/src/service/project.js
index 7c71495818..447c166d33 100644
--- a/kystudio/src/service/project.js
+++ b/kystudio/src/service/project.js
@@ -116,6 +116,14 @@ export default {
   updateIndexOptimization (data) {
     return Vue.resource(apiUrl + 'projects/' + data.project + '/garbage_cleanup_config').update(data)
   },
+  // 获取加速规则
+  getFavoriteRules (data) {
+    return Vue.resource(apiUrl + `projects/${data.project}/favorite_rules`).get(data)
+  },
+  // 更新加速规则
+  updateFavoriteRules (data) {
+    return Vue.resource(apiUrl + `projects/${data.project}/favorite_rules`).update(data)
+  },
   toggleEnableSCD (data) {
     return Vue.resource(apiUrl + 'projects/' + data.project + '/scd2_config').update(data)
   },
diff --git a/kystudio/src/store/model.js b/kystudio/src/store/model.js
index e08319e2e9..429e561209 100644
--- a/kystudio/src/store/model.js
+++ b/kystudio/src/store/model.js
@@ -1,6 +1,6 @@
 import api from './../service/api'
 import * as types from './types'
-import { transToGmtTime } from 'util/business'
+import { transToGmtTime } from '../util'
 export default {
   state: {
     modelsList: [],
@@ -313,6 +313,15 @@ export default {
     [types.DELETE_BATCH_INDEX] ({ commit }, params) {
       return api.model.deleteBatchIndex(params)
     },
+    [types.GET_MODEL_RECOMMENDATIONS] ({ commit }, paras) {
+      return api.model.getModelRecommendations(paras)
+    },
+    [types.ADOPT_MODEL_RECOMMENDATIONS] ({ commit }, paras) {
+      return api.model.adoptModelRecommendations(paras)
+    },
+    [types.CLEAR_MODEL_RECOMMENDATIONS] ({ commit }, paras) {
+      return api.model.clearModelRecommendations(paras)
+    },
     [types.GET_AGG_INDEX_CONTENTLIST] ({ commit }, paras) {
       return api.model.getAggIndexContentList(paras)
     },
@@ -388,6 +397,24 @@ export default {
     [types.UPDATE_MODEL_OWNER] ({ commit }, params) {
       return api.model.updateModelOwner(params)
     },
+    [types.GET_ALL_RECOMMENDATIONS] (_, params) {
+      return api.model.getRecommendations(params)
+    },
+    [types.RECOMMENDATION_COUNT_REFRESH] (_, params) {
+      return api.model.recommendationCountRefresh(params)
+    },
+    [types.DELETE_RECOMMENDATIONS] (_, params) {
+      return api.model.deleteRecommendations(params)
+    },
+    [types.ACCESS_RECOMMENDATIONS] (_, params) {
+      return api.model.acceptRecommendations(params)
+    },
+    [types.GET_RECOMMEND_DETAILS] (_, params) {
+      return api.model.getRecommendDetails(params)
+    },
+    [types.VALIDATE_RECOMMEND] (_, params) {
+      return api.model.validateRecommend(params)
+    },
     [types.FETCH_HIT_MODELS_LIST] (_, params) {
       return api.model.fetchHitModelsList(params)
     },
diff --git a/kystudio/src/store/project.js b/kystudio/src/store/project.js
index 8241593a2b..917c702859 100644
--- a/kystudio/src/store/project.js
+++ b/kystudio/src/store/project.js
@@ -1,9 +1,8 @@
 import api from './../service/api'
 import * as types from './types'
-import { cacheSessionStorage, cacheLocalStorage } from 'util/index'
+import { cacheSessionStorage, cacheLocalStorage } from '../util/index'
 import { getAvailableOptions } from '../util/specParser'
-import { fromObjToArr } from '../util'
-import { handleError } from '../util/business'
+import { fromObjToArr, handleError } from '../util'
 
 export default {
   state: {
@@ -37,6 +36,9 @@ export default {
     [types.SAVE_DEFAULT_CONFIG_LIST]: function (state, { list }) {
       state.defaultConfigList = list
     },
+    [types.UPDATE_PROJECT_SEMI_AUTOMATIC_STATUS]: function (state, result) {
+      state.isSemiAutomatic = result
+    },
     [types.SET_PROJECT]: function (state, project) {
       cacheSessionStorage('preProjectName', state.selected_project) // 储存上一次选中的project
       cacheSessionStorage('projectName', project)
@@ -53,11 +55,13 @@ export default {
           if (p.name === selectedProject) {
             hasMatch = true
             state.selected_project = p.name // 之前没这句,在其他tab 切换了project,顶部会不变
+            state.isSemiAutomatic = p.override_kylin_properties && p.override_kylin_properties['kylin.metadata.semi-automatic-mode'] === 'true' // 获取当前project 是否含有半自动的标志,且项目是专家模式
             state.projectDefaultDB = p.default_database
           }
         })
         if (!hasMatch) {
           state.selected_project = state.allProject[0].name
+          state.isSemiAutomatic = state.allProject[0].override_kylin_properties && state.allProject[0].override_kylin_properties['kylin.metadata.semi-automatic-mode'] === 'true'
           cacheSessionStorage('projectName', state.selected_project)
           cacheLocalStorage('projectName', state.selected_project)
           state.projectDefaultDB = state.allProject[0].default_database
@@ -232,6 +236,7 @@ export default {
         commit(types.CACHE_PROJECT_DEFAULT_DB, {projectDefaultDB: response.data.data.default_database})
         commit(types.CACHE_PROJECT_PUSHDOWN_CONFIG, {projectPushdownConfig: response.data.data.push_down_enabled})
         commit(types.CACHE_PROJECT_EXCLUDE_CONFIG, {projectExcludeTableConfig: response.data.data.table_exclusion_enabled})
+        commit(types.UPDATE_PROJECT_SEMI_AUTOMATIC_STATUS, response.data.data.semi_automatic_mode)
         commit(types.UPDATE_SCD2_ENABLE, response.data.data.scd2_enabled || false)
         commit(types.UPDATE_SNAPSHOT_MANUAL_ENABLE, response.data.data.snapshot_manual_management_enabled || false)
         commit(types.UPDATE_MULTI_PARTITION_ENABLE, response.data.data.multi_partition_enabled || false)
@@ -293,6 +298,9 @@ export default {
     [types.UPDATE_INDEX_OPTIMIZATION]: function ({ commit }, para) {
       return api.project.updateIndexOptimization(para)
     },
+    [types.GET_FAVORITE_RULES]: function (_, para) {
+      return api.project.getFavoriteRules(para)
+    },
     [types.UPDATE_FAVORITE_RULES]: function (_, para) {
       return api.project.updateFavoriteRules(para)
     },
@@ -352,6 +360,11 @@ export default {
       }
       return {override_kylin_properties: {}}
     },
+    isAutoProject: (state, getters) => {
+      if (getters.currentProjectData) {
+        return false
+      }
+    },
     selectedProjectDatasource: (state, getters, rootState) => {
       let datasourceKey = null
 
diff --git a/kystudio/src/store/system.js b/kystudio/src/store/system.js
index 9f8f1b8052..de100728c2 100644
--- a/kystudio/src/store/system.js
+++ b/kystudio/src/store/system.js
@@ -51,6 +51,7 @@ export default {
     jobLogs: '',
     showRevertPasswordDialog: 'true',
     isEditForm: false,
+    recommendationPageSize: 0,
     isShowGlobalAlter: false,
     dimMeasNameMaxLength: 300,
     favoriteImportSqlMaxSize: 1000,
@@ -178,6 +179,7 @@ export default {
           commit(types.GET_CONF_BY_NAME, {name: 'kylin.metadata.random-admin-password.enabled', key: 'showRevertPasswordDialog', defaultValue: 'true'})
           commit(types.GET_CONF_BY_NAME, {name: 'kylin.model.dimension-measure-name.max-length', key: 'dimMeasNameMaxLength', defaultValue: 300})
           commit(types.GET_CONF_BY_NAME, {name: 'kylin.model.suggest-model-sql-limit', key: 'favoriteImportSqlMaxSize'})
+          commit(types.GET_CONF_BY_NAME, { name: 'kylin.model.recommendation-page-size', key: 'recommendationPageSize' })
           commit(types.GET_CONF_BY_NAME, {name: 'resource_group_enabled', key: 'resourceGroupEnabled', defaultValue: 'false'})
           commit(types.GET_CONF_BY_NAME, {name: 'kylin.query.query-history-download-max-size', key: 'queryDownloadMaxSize', defaultValue: 100000})
           commit(types.GET_CONF_BY_NAME, {name: 'kylin.second-storage.class', key: 'isShowSecondStorage'})
diff --git a/kystudio/src/store/types.js b/kystudio/src/store/types.js
index cbd7e45136..3722661906 100644
--- a/kystudio/src/store/types.js
+++ b/kystudio/src/store/types.js
@@ -44,6 +44,7 @@ export const UPDATE_PROJECT_DATASOURCE = 'UPDATE_PROJECT_DATASOURCE'
 export const RESET_PROJECT_CONFIG = 'RESET_PROJECT_CONFIG'
 export const UPDATE_DEFAULT_DB_SETTINGS = 'UPDATE_DEFAULT_DB_SETTINGS'
 export const CACHE_PROJECT_DEFAULT_DB = 'CACHE_PROJECT_DEFAULT_DB'
+export const UPDATE_PROJECT_SEMI_AUTOMATIC_STATUS = 'UPDATE_PROJECT_SEMI_AUTOMATIC_STATUS'
 export const UPDATE_YARN_QUEUE = 'UPDATE_YARN_QUEUE'
 export const GET_ACL_PERMISSION = 'GET_ACL_PERMISSION'
 export const CHECK_SSB = 'CHECK_SSB'
@@ -51,6 +52,7 @@ export const IMPORT_SSB_DATABASE = 'IMPORT_SSB_DATABASE'
 export const UPDATE_EXPOSE_CC_CONFIG = 'UPDATE_EXPOSE_CC_CONFIG'
 export const UPDATE_KERBEROS_CONFIG = 'UPDATE_KERBEROS_CONFIG'
 export const UPDATE_INDEX_OPTIMIZATION = 'UPDATE_INDEX_OPTIMIZATION'
+export const GET_FAVORITE_RULES = 'GET_FAVORITE_RULES'
 export const UPDATE_FAVORITE_RULES = 'UPDATE_FAVORITE_RULES'
 export const TOGGLE_ENABLE_SCD = 'TOGGLE_ENABLE_SCD'
 export const GET_SCD2_MODEL = 'GET_SCD2_MODEL'
@@ -283,6 +285,12 @@ export const DOWNLOAD_MODELS_METADATA = 'DOWNLOAD_MODELS_METADATA'
 export const DOWNLOAD_MODELS_METADATA_BLOB = 'DOWNLOAD_MODELS_METADATA_BLOB'
 export const UPLOAD_MODEL_METADATA_FILE = 'UPLOAD_MODEL_METADATA_FILE'
 export const IMPORT_MODEL_METADATA_FILE = 'IMPORT_MODEL_METADATA_FILE'
+export const GET_ALL_RECOMMENDATIONS = 'GET_ALL_RECOMMENDATIONS'
+export const RECOMMENDATION_COUNT_REFRESH = 'RECOMMENDATION_COUNT_REFRESH'
+export const DELETE_RECOMMENDATIONS = 'DELETE_RECOMMENDATIONS'
+export const ACCESS_RECOMMENDATIONS = 'ACCESS_RECOMMENDATIONS'
+export const GET_RECOMMEND_DETAILS = 'GET_RECOMMEND_DETAILS'
+export const VALIDATE_RECOMMEND = 'VALIDATE_RECOMMEND'
 export const RESET_MODAL_STATE = 'RESET_MODAL_STATE'
 export const GET_STREAMING_JOB = 'GET_STREAMING_JOB'
 export const CHANGE_STREAMING_JOB_STATUS = 'CHANGE_STREAMING_JOB_STATUS'
@@ -298,6 +306,9 @@ export const EDIT_TABLE_INDEX = 'EDIT_TABLE_INDEX'
 export const DELETE_TABLE_INDEX = 'DELETE_TABLE_INDEX'
 export const ADD_TABLE_INDEX = 'ADD_TABLE_INDEX'
 export const BUILD_INDEX = 'BUILD_INDEX'
+export const GET_MODEL_RECOMMENDATIONS = 'GET_MODEL_RECOMMENDATIONS'
+export const ADOPT_MODEL_RECOMMENDATIONS = 'ADOPT_MODEL_RECOMMENDATIONS'
+export const CLEAR_MODEL_RECOMMENDATIONS = 'CLEAR_MODEL_RECOMMENDATIONS'
 export const GET_AGG_INDEX_CONTENTLIST = 'GET_AGG_INDEX_CONTENTLIST'
 export const GET_TABLE_INDEX_CONTENTLIST = 'GET_TABLE_INDEX_CONTENTLIST'
 export const GET_INDEX_CONTENTLIST = 'GET_INDEX_CONTENTLIST'
diff --git a/kystudio/src/util/business.js b/kystudio/src/util/business.js
index d170361201..a651c0cdd9 100644
--- a/kystudio/src/util/business.js
+++ b/kystudio/src/util/business.js
@@ -65,13 +65,19 @@ export function loadingBox () {
 }
 // 确认弹窗
 export function kylinConfirm (content, para, title) {
-  var dialogTitle = title || window.kylinVm.$t('kylinLang.common.tip')
-  var dialogPara = para || {type: 'warning'}
+  const dialogTitle = title || window.kylinVm.$t('kylinLang.common.tip')
+  const dialogPara = para || {type: 'warning'}
   return MessageBox.confirm(content, dialogTitle, dialogPara)
 }
-
+export function kylinWarn (content, para) {
+  const dialogPara = para || {
+    type: 'warning',
+    showCancelButton: false
+  }
+  return MessageBox.confirm(content, window.kylinVm.$t('kylinLang.common.tip'), dialogPara)
+}
 export function kylinMessage (content, para) {
-  var messagePara = Object.assign({
+  const messagePara = Object.assign({
     type: 'success',
     message: content,
     duration: 3000,