Răsfoiți Sursa

feat: 实现template Management部分页面

Jack Zhou 1 lună în urmă
părinte
comite
df85c410ae

+ 10 - 0
src/router/index.ts

@@ -111,6 +111,16 @@ const router = createRouter({
           name: 'PromptConfiguration',
           component: () => import('../views/PromptConfiguration')
         },
+        {
+          path: '/template-management',
+          name: 'Template Management',
+          component: () => import('../views/TemplateManagement')
+        },
+        {
+          path: '/create-report-template',
+          name: 'Create Report Template',
+          component: () => import('../views/TemplateManagement/src/components/CreateReportTemplate')
+        },
         {
           path: '/system-message',
           name: 'System Message',

+ 5 - 3
src/stores/modules/breadCrumb.ts

@@ -19,6 +19,7 @@ const whiteList = [
   'Configurations',
   'Create New Booking',
   'Destination Create New Rule',
+  "Create Report Template"
 ]
 
 export const useBreadCrumb = defineStore('breadCrumb', {
@@ -99,7 +100,7 @@ export const useBreadCrumb = defineStore('breadCrumb', {
         ]
       } else if (toRoute.name === 'Destination Create New Rule') {
         let label = ''
-        if(toRoute.query.a != undefined) {
+        if (toRoute.query.a != undefined) {
           label = 'Modify Rule'
         } else {
           label = 'Create New Rule'
@@ -121,8 +122,9 @@ export const useBreadCrumb = defineStore('breadCrumb', {
             query: toRoute.query
           }
         ]
-      } else if (toRoute.name === 'Create New Booking') {let label = ''
-        if(toRoute.query.a != undefined) {
+      } else if (toRoute.name === 'Create New Booking') {
+        let label = ''
+        if (toRoute.query.a != undefined) {
           label = 'Modify Booking'
         } else {
           label = 'Create New Booking'

+ 85 - 81
src/views/Layout/src/components/Menu/MenuView.vue

@@ -11,89 +11,93 @@ const userStore = useUserStore()
 const isCollapse = defineModel<boolean>()
 
 const menuList = ref()
-watch(
-  () => userStore.userInfo?.uname,
-  () => {
-    getMenuList()
-  }
-)
+// watch(
+//   () => userStore.userInfo?.uname,
+//   () => {
+//     getMenuList()
+//   }
+// )
 const getMenuList = () => {
-  $api.getMenuList().then((res) => {
-    if (res.code === 200) {
-      menuList.value = res.data
-    }
-  })
-  // menuList.value = [
-  //   {
-  //     index: '1',
-  //     label: 'Dashboard',
-  //     icon: 'icon_data_fill_b',
-  //     path: '/dashboard'
-  //   },
-  //   {
-  //     index: '2',
-  //     label: 'Booking',
-  //     icon: 'icon_booking__fill_b',
-  //     // path: '/booking',
-  //     type: 'list',
-  //     children: [
-  //       {
-  //         index: '2-1',
-  //         label: 'Booking Management',
-  //         path: '/booking'
-  //       },
-  //       {
-  //         index: '2-2',
-  //         label: 'Destination Delivery',
-  //         path: '/destination-delivery'
-  //       }
-  //     ]
-  //   },
-  //   {
-  //     index: '3',
-  //     label: 'Tracking',
-  //     icon: 'icon_tracking__fill_b',
-  //     path: '/tracking'
-  //   },
-  //   {
-  //     index: '4',
-  //     label: 'System Management',
-  //     icon: 'icon_system__management_fill_b',
-  //     type: 'list',
-  //     children: [
-  //       {
-  //         index: '4-1',
-  //         label: 'System Message',
-  //         path: '/system-message'
-  //       },
-  //       {
-  //         index: '4-2',
-  //         label: 'System Settings',
-  //         path: '/SystemSettings'
-  //       },
-  //       {
-  //         index: '4-3',
-  //         label: 'Chat Log',
-  //         path: '/chat-log'
-  //       },
-  //       {
-  //         index: '4-4',
-  //         label: 'AI API Log',
-  //         path: '/ai-api-log'
-  //       },
-  //       {
-  //         index: '4-5',
-  //         label: 'Operation Log',
-  //         path: '/Operationlog'
-  //       },
-  //       {
-  //         index: '4-6',
-  //         label: 'Prompt Configuration',
-  //         path: '/PromptConfiguration'
-  //       }
-  //     ]
+  // $api.getMenuList().then((res) => {
+  //   if (res.code === 200) {
+  //     menuList.value = res.data
   //   }
-  // ]
+  // })
+  menuList.value = [
+    {
+      index: '1',
+      label: 'Dashboard',
+      icon: 'icon_data_fill_b',
+      path: '/dashboard'
+    },
+    {
+      index: '2',
+      label: 'Booking',
+      icon: 'icon_booking__fill_b',
+      type: 'list',
+      children: [
+        {
+          index: '2-1',
+          label: 'Booking Management',
+          path: '/booking'
+        },
+        {
+          index: '2-2',
+          label: 'Destination Delivery',
+          path: '/destination-delivery'
+        }
+      ]
+    },
+    {
+      index: '3',
+      label: 'Tracking',
+      icon: 'icon_tracking__fill_b',
+      path: '/tracking'
+    },
+    {
+      index: '4',
+      label: 'System Management',
+      icon: 'icon_system__management_fill_b',
+      type: 'list',
+      children: [
+        {
+          index: '4-7',
+          label: 'Template Management',
+          path: '/template-management'
+        },
+        {
+          index: '4-1',
+          label: 'System Message',
+          path: '/system-message'
+        },
+        {
+          index: '4-2',
+          label: 'System Settings',
+          path: '/SystemSettings'
+        },
+        {
+          index: '4-3',
+          label: 'Chat Log',
+          path: '/chat-log'
+        },
+        {
+          index: '4-4',
+          label: 'AI API Log',
+          path: '/ai-api-log'
+        },
+        {
+          index: '4-5',
+          label: 'Operation Log',
+          path: '/Operationlog'
+        },
+        {
+          index: '4-6',
+          label: 'Prompt Configuration',
+          path: '/PromptConfiguration'
+        }
+      ]
+    }
+  ]
 }
 getMenuList()
 //监听窗口大小

+ 1 - 0
src/views/TemplateManagement/index.ts

@@ -0,0 +1 @@
+export { default } from './src/TemplateManagement.vue'

+ 176 - 0
src/views/TemplateManagement/src/TemplateManagement.vue

@@ -0,0 +1,176 @@
+<script lang="ts" setup>
+import { useCalculatingHeight } from '@/hooks/calculatingHeight'
+import TableView from './components/TableView'
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+const filterRef: Ref<HTMLElement | null> = ref(null)
+const containerHeight = useCalculatingHeight(document.documentElement, 290, [filterRef])
+const searchData = ref({
+  text_search: '',
+  request_date_start: '',
+  request_date_end: '',
+  ai_model: '',
+  response_duration_type: '',
+  response_duration_num: null
+})
+
+const aiModelList = [
+  {
+    label: 'Deepseek',
+    value: 'Deepseek'
+  },
+  {
+    label: 'Claude',
+    value: 'Claude'
+  }
+]
+
+const comparatorList = [
+  {
+    label: '>=',
+    value: 'thanOrEqual'
+  },
+  {
+    label: '=',
+    value: 'equal'
+  },
+  {
+    label: '<=',
+    value: 'lessOrEqual'
+  }
+]
+
+const tableRef = ref()
+
+const Search = () => {
+  tableRef.value.SearchOperationLog(searchData.value)
+}
+
+const handleCreate = () => {
+  // Navigate to the Create Report Template page
+  router.push('/create-report-template')
+}
+</script>
+<template>
+  <div class="dashboard">
+    <div class="Title">
+      <span>Report Template Management</span>
+      <el-button class="el-button--main" @click="handleCreate">
+        <span class="font_family icon-icon_add_b"></span> Create New Report Template</el-button
+      >
+    </div>
+    <div class="display">
+      <div class="heaer_top">
+        <div class="input-tips_filter">
+          <el-input
+            placeholder="Search report name"
+            v-model="searchData.text_search"
+            class="log_input"
+          >
+            <template #prefix>
+              <span class="iconfont_icon">
+                <svg class="iconfont icon_dark" aria-hidden="true">
+                  <use xlink:href="#icon-icon_search_b"></use>
+                </svg>
+              </span>
+            </template>
+          </el-input>
+        </div>
+
+        <div class="tips_filter">
+          <el-select v-model="searchData.ai_model" clearable placeholder="Is Active">
+            <el-option
+              v-for="item in aiModelList"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </div>
+        <div class="tips_filter">
+          <el-select v-model="searchData.ai_model" clearable placeholder="Application Scope">
+            <el-option
+              v-for="item in aiModelList"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </div>
+        <div class="tips_filter">
+          <el-select v-model="searchData.ai_model" clearable placeholder="Party ID">
+            <el-option
+              v-for="item in aiModelList"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </div>
+
+        <el-button class="el-button--dark" @click="Search">Search</el-button>
+      </div>
+    </div>
+    <TableView :height="containerHeight" ref="tableRef"></TableView>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.Title {
+  display: flex;
+  justify-content: space-between;
+  height: 68px;
+  border-bottom: 1px solid var(--color-border);
+  font-size: var(--font-size-6);
+  font-weight: 700;
+  padding: 0 24px;
+  align-items: center;
+}
+.heaer_top {
+  margin-top: 6.57px;
+  margin-bottom: 8px;
+  padding-right: 8px;
+  display: flex;
+}
+
+.display {
+  border: 1px solid var(--color-border);
+  border-width: 0 0 1px 0;
+  padding-left: 23.52px;
+}
+
+.tips_filter {
+  flex: 1;
+  height: 30px;
+  max-width: 190px;
+  margin-right: 8px;
+}
+.input-tips_filter {
+  flex: 1;
+  max-width: 320px;
+  height: 32px;
+  margin-right: 8px;
+  :deep(.el-input__wrapper) {
+    height: 32px;
+  }
+}
+.date-tips_filter {
+  flex: 1;
+  max-width: 250px;
+  height: 32px;
+  margin-right: 8px;
+}
+.comparator-tips_filter {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  max-width: 260px;
+  height: 32px;
+  margin-right: 8px;
+}
+.dashboard {
+  position: relative;
+  background-color: var(--color-mode);
+}
+</style>

+ 1 - 0
src/views/TemplateManagement/src/components/CreateReportTemplate/index.ts

@@ -0,0 +1 @@
+export { default } from './src/CreateReportTemplate.vue'

+ 408 - 0
src/views/TemplateManagement/src/components/CreateReportTemplate/src/CreateReportTemplate.vue

@@ -0,0 +1,408 @@
+<script lang="ts" setup>
+import { useRouter } from 'vue-router'
+import partyIDSelect from './components/partyIDSelect.vue'
+import GroupNameSelect from './components/GroupNameSelect.vue'
+
+const router = useRouter()
+const filterRef: Ref<HTMLElement | null> = ref(null)
+const searchData = ref({
+  text_search: '',
+  request_date_start: '',
+  request_date_end: '',
+  ai_model: '',
+  response_duration_type: '',
+  response_duration_num: null
+})
+
+const infoData = ref({
+  reportName: '',
+  reportDescription: ''
+})
+
+const tableRef = ref()
+
+const Search = () => {
+  tableRef.value.SearchOperationLog(searchData.value)
+}
+
+const handleCreate = () => {
+  // Navigate to the Create Report Template page
+  router.push('/create-report-template')
+}
+
+const fieldsList = ref([])
+
+const CustomizeColumnsRef = ref()
+// 打开定制表格弹窗
+const handleCustomizeColumns = () => {
+  const params = {
+    getData: {
+      action: 'ocean_booking',
+      operate: 'setting_display'
+    },
+    saveData: {
+      action: 'ajax',
+      operate: 'save_setting_display',
+      model_name: 'Booking_Search'
+    }
+  }
+  CustomizeColumnsRef.value.openDialog(params, -220)
+}
+// 定制表格
+const customizeColumns = async () => {
+  await $api.getBookingTableColumns().then((res: any) => {
+    if (res.code === 200) {
+      fieldsList.value = res.data.BookingTableColumns
+    }
+  })
+}
+
+const radioa = ref('1')
+const detailRef: Ref<HTMLElement | null> = ref(null)
+watch(radioa, (newVal) => {
+  if (newVal === '2') {
+    // 等待下一个渲染周期结束后,获取detailRef的高度
+    nextTick(() => {
+      if (detailRef.value) {
+        detailRef.value.scrollIntoView({
+          behavior: 'smooth', // 平滑滚动
+          block: 'start' // 滚动到顶部对齐
+        })
+      }
+    })
+  }
+})
+</script>
+<template>
+  <div class="dashboard">
+    <div class="Title">
+      <span>Create New Report Template</span>
+      <div class="button-group">
+        <el-button type="default" @click="handleCreate">
+          <span class="font_family icon-icon_return_b" style="margin-right: 3px"></span
+          >Cancel</el-button
+        >
+        <el-button :disabled="true" class="el-button--main" @click="handleCreate">
+          <span class="font_family icon-icon_save_b" style="margin-right: 3px"></span
+          >Save</el-button
+        >
+      </div>
+    </div>
+    <div class="display">
+      <div class="basic-info template-box">
+        <div class="header">Basic Report Information</div>
+        <div class="content-box">
+          <div class="info-item">
+            <div class="label">
+              <span style="color: var(--color-danger)">*</span>
+              <span>Report Name</span>
+            </div>
+            <el-input v-model="infoData.reportName" placeholder="Please enter..."></el-input>
+          </div>
+          <div class="info-item">
+            <div class="label">
+              <span style="color: var(--color-danger)">*</span>
+              <span>Report Description</span>
+            </div>
+            <el-input
+              type="textarea"
+              v-model="infoData.reportDescription"
+              placeholder="Please enter..."
+            ></el-input>
+          </div>
+        </div>
+      </div>
+      <div class="fields-configuration template-box">
+        <div class="header">
+          <span>Report Fields Configuration</span>
+          <el-button
+            v-if="fieldsList.length > 0"
+            class="el-button--dark"
+            @click="handleCustomizeColumns"
+            style="margin-left: auto; width: 110px; padding-top: 11px"
+          >
+            <span style="margin-right: 3px" class="font_family icon-icon_add_b"></span>Add Field
+          </el-button>
+        </div>
+        <div class="content-box">
+          <div class="empty-box" v-if="fieldsList.length === 0">
+            <el-button class="el-button--dark" @click="handleCustomizeColumns">
+              <span class="font_family icon-icon_add_b"></span>Add/Edit Field
+            </el-button>
+            <p>No field selected. click “Add Field” to get started.</p>
+          </div>
+          <div class="fields-list" v-else>
+            <div class="field-item" v-for="(field, index) in fieldsList" :key="index">
+              <div class="label">
+                <span style="font-weight: 700">[{{ field.title }}]</span>
+                <span style="margin-left: 8px">{{ field.field }}</span>
+              </div>
+              <el-input class="display-name" placeholder="Display Name in Report"></el-input>
+              <div class="actions">
+                <div><el-checkbox>Filter</el-checkbox> <el-checkbox>Sort</el-checkbox></div>
+                <span class="font_family icon-icon_delete_b"></span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="report-access-control template-box">
+        <div class="header">Report Access Control</div>
+        <div class="content-box">
+          <el-radio-group class="radio-group" v-model="radioa">
+            <el-radio class="radio-item" value="1">
+              <template #default>
+                <div class="radio-content">
+                  <p class="label">All Users</p>
+                  <p class="description">
+                    This report will be available to all users in the system
+                  </p>
+                </div>
+              </template>
+            </el-radio>
+            <el-radio class="radio-item specific-roles" value="2">
+              <template #default>
+                <div class="radio-content">
+                  <div class="top-options">
+                    <p class="label">Specific Roles</p>
+                    <p class="description">Restrict access to specific user roles</p>
+                  </div>
+                  <div class="extended-filter" v-if="radioa === '2'" ref="detailRef">
+                    <div class="dividing-line"></div>
+                    <div class="filter-item" style="margin-bottom: 16px">
+                      <div class="label">
+                        <span style="color: var(--color-danger)">*</span>
+                        <span>Party ID</span>
+                      </div>
+                      <partyIDSelect></partyIDSelect>
+                    </div>
+                    <div class="filter-item">
+                      <div class="label">
+                        <span style="color: var(--color-danger)">*</span>
+                        <span>Group Name</span>
+                      </div>
+                      <GroupNameSelect></GroupNameSelect>
+                    </div>
+                  </div>
+                </div>
+              </template>
+            </el-radio>
+          </el-radio-group>
+        </div>
+      </div>
+    </div>
+    <CustomizeColumns @customize="customizeColumns" ref="CustomizeColumnsRef" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.Title {
+  position: sticky;
+  top: 0;
+  z-index: 100;
+  display: flex;
+  justify-content: space-between;
+  height: 68px;
+  border-bottom: 1px solid var(--color-border);
+  font-size: var(--font-size-6);
+  font-weight: 700;
+  padding: 0 24px;
+  align-items: center;
+  background-color: var(--color-mode);
+}
+.heaer_top {
+  margin-top: 6.57px;
+  margin-bottom: 8px;
+  padding-right: 8px;
+  display: flex;
+}
+
+.display {
+  max-height: calc(100vh - 140px);
+  border: 1px solid var(--color-border);
+  border-bottom: none;
+  border-width: 0 0 1px 0;
+  padding: 16px 24px 8px;
+}
+.template-box {
+  margin-bottom: 8px;
+  border: 1px solid var(--color-border);
+  border-radius: 12px;
+  overflow: hidden;
+  .header {
+    height: 48px;
+    border-bottom: 1px solid var(--color-border);
+    display: flex;
+    align-items: center;
+    padding: 0 16px;
+    border-radius: 12px 12px 0 0;
+    background-color: var(--color-header-bg);
+    font-size: 18px;
+    font-weight: bold;
+  }
+  .content-box {
+    height: 100%;
+    padding: 8px 16px 16px;
+  }
+}
+.fields-configuration {
+  div.content-box {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    min-height: 272px;
+    max-height: 400px;
+    width: 100%;
+    padding-bottom: 8px;
+    padding-right: 0px;
+    // overflow: auto;
+    .empty-box {
+      width: 100%;
+      text-align: center;
+      p {
+        margin-top: 12px;
+        color: var(--color-neutral-2);
+      }
+    }
+    .fields-list {
+      width: 100%;
+      max-height: 400px;
+      padding-top: 8px;
+      padding-right: 16px;
+      overflow: auto;
+      .field-item {
+        display: flex;
+        align-items: center;
+        margin-bottom: 8px;
+        .label {
+          flex: 1;
+        }
+        .display-name {
+          flex: 1.3;
+          margin: 0 16px;
+          :deep(.el-input__wrapper) {
+            height: 32px;
+          }
+        }
+        .actions {
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+          width: 160px;
+          .el-checkbox {
+            margin-right: 16px;
+            :deep(.el-checkbox__inner) {
+              height: 16px;
+              width: 16px;
+              &::after {
+                border-width: 2px;
+                height: 9px;
+                width: 5px;
+                left: 4px;
+              }
+            }
+            :deep(.el-checkbox__label) {
+              padding-left: 4px;
+              line-height: 2;
+            }
+          }
+          .font_family {
+            float: right;
+            cursor: pointer;
+          }
+        }
+      }
+    }
+  }
+}
+.basic-info {
+  .info-item {
+    &:first-child {
+      margin-bottom: 16px;
+    }
+    .label {
+      margin-bottom: 4px;
+      font-size: 12px;
+      span {
+        color: var(--color-neutral-2);
+      }
+    }
+  }
+}
+
+.report-access-control {
+  .content-box {
+    .radio-group {
+      display: flex;
+      align-items: center;
+      flex-direction: column;
+      gap: 8px;
+    }
+    .radio-item {
+      align-items: flex-start;
+      width: 100%;
+      // min-height: 80px;
+      height: auto;
+      margin-right: 24px;
+      margin-top: 4px;
+      padding: 20px 16px;
+      font-size: 14px;
+      border: 1px solid var(--color-border);
+      border-radius: 12px;
+      .radio-content {
+        height: auto;
+      }
+      // .top-options {
+      .label {
+        font-weight: 700;
+      }
+      .description {
+        margin-top: 8px;
+        font-size: 12px;
+        color: var(--color-neutral-2);
+      }
+      // }
+    }
+    .specific-roles {
+      position: relative;
+      .dividing-line {
+        position: absolute;
+        left: 0px;
+        top: 66px;
+        margin: 12px 0;
+        height: 1px;
+        width: 100%;
+        background-color: var(--color-border);
+      }
+    }
+    .extended-filter {
+      margin-top: 28px;
+      border-radius: 6px;
+
+      .filter-item {
+        .label {
+          margin-bottom: 4px;
+          span {
+            font-size: 12px;
+            color: var(--color-neutral-2);
+          }
+        }
+      }
+    }
+    // .radio-item.specific-roles {
+    //   padding: 0;
+    // }
+  }
+}
+
+.dashboard {
+  position: relative;
+  background-color: var(--color-mode);
+  .button-group {
+    .el-button {
+      height: 40px;
+      padding: 8px 32px;
+    }
+  }
+}
+</style>

+ 161 - 0
src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/GroupNameSelect.vue

@@ -0,0 +1,161 @@
+<script setup lang="ts">
+interface ListItem {
+  value: string
+  label: string
+  checked: boolean
+}
+
+const list = ref<ListItem[]>([])
+const options = ref<ListItem[]>([])
+const selectData = ref<string[]>([])
+const loading = ref(false)
+
+const states = [
+  'GRNAME000010',
+  'GRNAME000012',
+  'GRNAME000013',
+  'GRNAME000014',
+  'GRNAME000014',
+  'GRNAME000015',
+  'GRNAME000016',
+  'GRNAME000017',
+  'GRNAME000018',
+  'GRNAME000019'
+]
+
+onMounted(() => {
+  list.value = states.map((item) => ({
+    value: `value:${item}LBR700011,DKCN000013,DKCN000011,DKCN000010,DKCN000010,DKCN000010,DKCN000010,DKCN000010,DKCN000010,LBR700011,DKCN000013,DKCN000011,DKCN000010,DKCN000010,DKCN000010,DKCN000010,DKCN000010,DKCN000010`,
+    label: `label:${item}`,
+    checked: false
+  }))
+  options.value = [...list.value]
+})
+
+// 搜索方法
+const remoteMethod = (query: string) => {
+  if (query) {
+    loading.value = true
+    setTimeout(() => {
+      loading.value = false
+      options.value = list.value.filter((item) => {
+        return item.label.toLowerCase().includes(query.toLowerCase())
+      })
+      syncCheckedState()
+    }, 200)
+  } else {
+    options.value = [...list.value]
+    syncCheckedState()
+  }
+}
+
+const syncCheckedState = () => {
+  options.value.forEach((item) => {
+    item.checked = selectData.value.includes(item.value)
+  })
+}
+
+watch(
+  () => selectData.value,
+  () => {
+    syncCheckedState()
+  },
+  { immediate: true, deep: true }
+)
+
+const handleCheckboxChange = (item: ListItem) => {
+  // 先翻转状态
+  item.checked = !item.checked
+
+  const index = selectData.value.indexOf(item.value)
+  if (item.checked && index === -1) {
+    selectData.value.push(item.value)
+  } else if (!item.checked && index > -1) {
+    selectData.value.splice(index, 1)
+  }
+}
+
+const testRef = ref(null)
+onMounted(() => {
+  console.log('testRef offsetWidth:', testRef.value?.$el?.offsetWidth)
+})
+</script>
+
+<template>
+  <el-select
+    v-model="selectData"
+    multiple
+    filterable
+    remote
+    reserve-keyword
+    placeholder="Please enter a keyword"
+    :remote-method="remoteMethod"
+    :loading="loading"
+    style="width: 100%"
+    ref="testRef"
+    popper-class="group-name-select-popper"
+  >
+    <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value">
+      <div class="select-option">
+        <el-checkbox
+          :model-value="item.checked"
+          @change="handleCheckboxChange(item)"
+          @click.stop
+          @mousedown.prevent
+          style="height: 22px"
+        >
+          {{ item.label }}
+        </el-checkbox>
+        <span :style="{ width: testRef ? testRef?.$el?.offsetWidth - 70 + 'px' : 'auto' }">{{
+          item.value
+        }}</span>
+      </div>
+    </el-option>
+  </el-select>
+</template>
+
+<style lang="scss" scoped>
+.select-option {
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  align-items: flex-start;
+  max-width: 100%;
+  :deep(.el-checkbox__label) {
+    font-weight: 700;
+  }
+  & > span {
+    margin-left: 24px;
+    // width: 100%;
+    white-space: wrap;
+    font-size: 12px;
+    line-height: 16px;
+    color: var(--color-neutral-2);
+  }
+}
+
+:deep(.el-checkbox__inner) {
+  height: 16px;
+  width: 16px;
+  &::after {
+    border-width: 2px;
+    height: 9px;
+    width: 5px;
+    left: 4px;
+    top: 0;
+  }
+}
+</style>
+
+<style lang="scss">
+.group-name-select-popper {
+  .el-select-dropdown__item {
+    height: auto;
+    padding: 8px;
+  }
+}
+// .my-select-popper {
+//   width: 320px !important;
+//   min-width: unset !important;
+// }
+</style>

+ 128 - 0
src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/PartyIDSelect.vue

@@ -0,0 +1,128 @@
+<script setup lang="ts">
+interface ListItem {
+  value: string
+  label: string
+  checked: boolean
+}
+
+const list = ref<ListItem[]>([])
+const options = ref<ListItem[]>([])
+const selectData = ref<string[]>([])
+const loading = ref(false)
+
+const states = ['Alabama', 'Alaska', 'Arizona', 'Arkansas']
+
+onMounted(() => {
+  list.value = states.map((item) => ({
+    value: `value:${item}`,
+    label: `label:${item}`,
+    checked: false
+  }))
+  options.value = [...list.value]
+})
+
+// 搜索方法
+const remoteMethod = (query: string) => {
+  if (query) {
+    loading.value = true
+    setTimeout(() => {
+      loading.value = false
+      options.value = list.value.filter((item) => {
+        return item.label.toLowerCase().includes(query.toLowerCase())
+      })
+      syncCheckedState()
+    }, 200)
+  } else {
+    options.value = [...list.value]
+    syncCheckedState()
+  }
+}
+
+const syncCheckedState = () => {
+  options.value.forEach((item) => {
+    item.checked = selectData.value.includes(item.value)
+  })
+}
+
+watch(
+  () => selectData.value,
+  () => {
+    syncCheckedState()
+  },
+  { immediate: true, deep: true }
+)
+
+const handleCheckboxChange = (item: ListItem) => {
+  // 先翻转状态
+  item.checked = !item.checked
+
+  const index = selectData.value.indexOf(item.value)
+  if (item.checked && index === -1) {
+    selectData.value.push(item.value)
+  } else if (!item.checked && index > -1) {
+    selectData.value.splice(index, 1)
+  }
+}
+</script>
+
+<template>
+  <el-select
+    v-model="selectData"
+    multiple
+    filterable
+    remote
+    reserve-keyword
+    placeholder="Please enter a keyword"
+    :remote-method="remoteMethod"
+    :loading="loading"
+    style="width: 100%"
+    popper-class="part-id-select-popper"
+  >
+    <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value">
+      <div class="select-option">
+        <el-checkbox
+          :model-value="item.checked"
+          @change="handleCheckboxChange(item)"
+          @click.stop
+          @mousedown.prevent
+          style="width: 110px"
+        >
+          {{ item.label }}
+        </el-checkbox>
+        <span>{{ item.value }}</span>
+      </div>
+    </el-option>
+  </el-select>
+</template>
+
+<style lang="scss" scoped>
+.select-option {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  width: 100%;
+  & > span {
+    margin-left: 56px;
+    line-height: 16px;
+  }
+}
+
+:deep(.el-checkbox__inner) {
+  height: 16px;
+  width: 16px;
+  &::after {
+    border-width: 2px;
+    height: 9px;
+    width: 5px;
+    left: 4px;
+    top: 0;
+  }
+}
+</style>
+
+<style lang="scss">
+.part-id-select-popper {
+  width: 480px !important;
+  min-width: unset !important;
+}
+</style>

+ 1 - 0
src/views/TemplateManagement/src/components/TableView/index.ts

@@ -0,0 +1 @@
+export { default } from './src/TableView.vue'

+ 505 - 0
src/views/TemplateManagement/src/components/TableView/src/TableView.vue

@@ -0,0 +1,505 @@
+<script setup lang="ts">
+import { ref, nextTick, onMounted } from 'vue'
+import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
+import DownloadDialog from './components/DownloadDialog.vue'
+import { autoWidth } from '@/utils/table'
+import { useRowClickStyle } from '@/hooks/rowClickStyle'
+import dayjs from 'dayjs'
+import { formatTimezone, formatNumber } from '@/utils/tools'
+
+const props = defineProps({
+  height: {
+    type: Number,
+    default: 440
+  }
+})
+
+const tableOriginColumnsField = ref()
+const handleColumns = (columns: any, status?: string) => {
+  const newColumns = columns.map((item: any) => {
+    let curColumn: any = {
+      title: item.title,
+      field: item.field,
+      sortable: true,
+      minWidth: 120,
+      showOverflow: true
+    }
+    // 设置插槽
+    if (item.type === 'status' && status !== 'all') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'status' }
+      }
+    } else if (item.type === 'link' && status !== 'all') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'link' }
+      }
+    } else if (item.type === 'mode' && status !== 'all') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'mode' },
+        formatter: ({ cellValue }: any) => {
+          return cellValue
+        }
+      }
+    }
+    // 格式化
+    if (item.formatter === 'date' || item.formatter === 'dateTime') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => formatTimezone(cellValue)
+      }
+    }
+    return curColumn
+  })
+  return newColumns
+}
+
+// 获取表格列
+const getTableColumns = async () => {
+  tableLoadingColumn.value = true
+  await $api.getAIApiLogTableColumn().then((res: any) => {
+    if (res.code === 200) {
+      tableData.value.columns = [
+        { title: 'Action', width: 116, fixed: 'left', slots: { default: 'action' } },
+        ...handleColumns(res.data.OperationTableColumns)
+      ]
+      tableOriginColumnsField.value = res.data.OperationTableColumns
+    }
+  })
+  nextTick(() => {
+    tableRef.value && autoWidth(tableData.value, tableRef.value)
+    tableLoadingColumn.value = false
+    selectedNumber.value = 0
+    selectedTableData.value = []
+  })
+}
+
+const pageInfo = ref({ pageNo: 1, pageSize: 20, total: 0 })
+const tempSearch = ref()
+// 获得表格数据后赋值
+const assignTableData = (data: any) => {
+  tableData.value.data = data.searchData || []
+  pageInfo.value.total = Number(data.rc) || 0
+  tempSearch.value = data.tmp_search
+  // 拥有所有字段的表格
+  setTimeout(() => {
+    allTable.value.columns = handleColumns(tableData.value.columns, 'all')
+    allTable.value.data = data.searchData || []
+    // 为了让导出的表格列宽度自适应
+    nextTick(() => {
+      allTableRef.value && autoWidth(allTable.value, allTableRef.value)
+    })
+  }, 1000)
+}
+
+let searchdata: any = {}
+// 获取表格数据
+const getTableData = async (isPageChange?: boolean) => {
+  const rc = isPageChange ? pageInfo.value.total : -1
+  tableLoadingTableData.value = true
+  await $api
+    .getAIApiLogTableData({
+      cp: pageInfo.value.pageNo,
+      ps: pageInfo.value.pageSize,
+      rc,
+      ...searchdata
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        assignTableData(res.data)
+      }
+    })
+    .finally(() => {
+      selectedNumber.value = 0
+      selectedTableData.value = []
+      nextTick(() => {
+        tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableLoadingTableData.value = false
+      })
+    })
+}
+const SearchOperationLog = (val: any) => {
+  searchdata = val
+  tableLoadingTableData.value = true
+  $api
+    .getAIApiLogTableData({
+      cp: pageInfo.value.pageNo,
+      ps: pageInfo.value.pageSize,
+      rc: -1,
+      ...val
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        assignTableData(res.data)
+      }
+    })
+    .finally(() => {
+      selectedNumber.value = 0
+      selectedTableData.value = []
+      nextTick(() => {
+        tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableLoadingTableData.value = false
+      })
+    })
+}
+onMounted(() => {
+  //
+  Promise.all([getTableColumns(), getTableData(false)]).finally(() => {
+    nextTick(() => {
+      tableRef.value && autoWidth(tableData.value, tableRef.value)
+    })
+  })
+})
+
+const tableRef = ref<VxeGridInstance>()
+const tableData = ref<VxeGridProps<any>>({
+  border: true,
+  round: true,
+  columns: [],
+  data: [],
+  scrollY: { enabled: true, oSize: 20, gt: 30 },
+  emptyText: ' ',
+  showHeaderOverflow: true,
+  showOverflow: true,
+  headerRowStyle: {
+    backgroundColor: 'var(--color-table-header-bg)'
+  },
+  cellConfig: {
+    height: 40
+  },
+  headerCellConfig: {
+    height: 40
+  },
+  sortConfig: {
+    sortMethod: (params) => {
+      const { data, sortList } = params
+
+      // 如果没有排序条件,直接返回原数据
+      if (sortList.length === 0) return data
+
+      // 对数据进行多重排序
+      const sortedData = [...data].sort((a, b) => {
+        for (const { field, order } of sortList) {
+          const curColumn = tableOriginColumnsField.value.find((item: any) => item.field === field)
+          if (!curColumn) continue
+
+          const typeName = curColumn.type
+          const aValue = a[field]
+          const bValue = b[field]
+
+          const compareResult = (aValue: any, bValue: any) => {
+            // 如果 aValue 或 bValue 是 null 或 undefined,优先处理这些值
+            if (aValue == null && bValue == null) {
+              return 0 // 如果两个值都为 null 或 undefined,视为相等
+            } else if (aValue == null) {
+              return -1 // 如果 aValue 是 null,bValue 不是,则将 aValue 视为较小值
+            } else if (bValue == null) {
+              return 1 // 如果 bValue 是 null,aValue 不是,则将 bValue 视为较小值
+            }
+
+            if (typeName === 'datetime' || typeName === 'date' || typeName === 'time') {
+              return dayjs(aValue).unix() - dayjs(bValue).unix()
+            } else if (isNaN(Number(aValue)) || isNaN(Number(bValue))) {
+              return aValue.localeCompare(bValue)
+            } else {
+              return Number(aValue) - Number(bValue)
+            }
+          }
+
+          const result = compareResult(aValue, bValue)
+          if (result !== 0) {
+            return order === 'asc' ? result : -result
+          }
+        }
+
+        return 0 // 如果所有字段都相等
+      })
+
+      return sortedData
+    }
+  },
+  columnConfig: { resizable: true, useKey: true },
+  rowConfig: { isHover: true },
+  exportConfig: {
+    types: ['csv', 'html', 'txt', 'xlsx'],
+    modes: ['current', 'selected', 'all']
+  }
+})
+const allTableRef = ref<VxeGridInstance>()
+const allTable = ref<VxeGridProps<any>>({
+  columns: [],
+  data: [],
+  showHeaderOverflow: true,
+  showOverflow: true,
+  scrollY: { enabled: true, oSize: 5, gt: 2 },
+  scrollX: { enabled: true, gt: 2 },
+  exportConfig: {
+    types: ['csv', 'html', 'txt', 'xlsx'],
+    modes: ['current', 'selected', 'all']
+  }
+})
+
+// 实现行点击样式
+useRowClickStyle(tableRef)
+
+const downloadDialogRef = ref()
+const handleDownload = () => {
+  const curSelectedColumns: string[] = []
+  tableRef.value?.columns?.forEach((item: any) => {
+    if (item.field) {
+      curSelectedColumns.push(item.title)
+    }
+  })
+  downloadDialogRef.value.openDialog(
+    curSelectedColumns,
+    selectedNumber.value || pageInfo.value.total
+  )
+}
+
+const exportLoading = ref(false)
+// 获取导出表格数据
+const getExportTableData = (status: number) => {
+  // 如果有选中表格行数据,那么只到处选中的数据
+  if (selectedNumber.value > 0) {
+    exportTable(status)
+    return
+  }
+  exportLoading.value = true
+  const buildColumnString = (columns: any[]): string => {
+    return columns
+      .filter((item) => item.field)
+      .map((item) => item.title)
+      .join(',')
+  }
+
+  let column = ''
+  if (status === 1) {
+    column = buildColumnString(tableData.value.columns)
+  } else {
+    column = buildColumnString(allTable.value.columns)
+  }
+  $api
+    .getAIApiLogAllTableData({
+      selected_fields: column,
+      tmp_search: tempSearch.value
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        allTable.value.data = res.data.Data || []
+        nextTick(() => {
+          exportLoading.value = false
+          // 导出数据
+          exportTable(status)
+        })
+      }
+    })
+    .finally(() => {
+      exportLoading.value = false
+    })
+}
+// 导出表格 status: 1 导出当前表格 2 导出所有字段表格
+const exportTable = (status: number) => {
+  const exportConfig: any = {
+    type: 'xlsx',
+    message: false,
+    filename: `AI API Log_${dayjs().format('YYYYMMDDHH[h]mm[m]ss[s]')}`
+  }
+  if (status === 1) {
+    exportConfig.columnFilterMethod = ({ column }: any) => {
+      const index = tableData.value.columns.findIndex((item: any) => item.field === column.field)
+      // 排除复选框列
+      return column.field && index !== -1
+    }
+    exportConfig.columns = tableData.value.columns
+  }
+  if (selectedNumber.value > 0) {
+    exportConfig.dataFilterMethod = ({ row }: any) => {
+      const index = selectedTableData.value.findIndex(
+        (item: any) => item._X_ROW_KEY === row._X_ROW_KEY
+      )
+      return index !== -1
+    }
+  }
+  allTableRef.value?.exportData(exportConfig)
+}
+
+const tableLoadingColumn = ref(false)
+const tableLoadingTableData = ref(false)
+
+const selectedNumber = ref(0)
+const selectedTableData = ref([])
+// 复选框选中事件
+const handleCheckboxChange = ({ records }: any) => {
+  selectedNumber.value = records.length
+  selectedTableData.value = records
+}
+const handleCheckAllChange = ({ records }: any) => {
+  selectedNumber.value = records.length
+  selectedTableData.value = records
+}
+
+const logDialogRef = ref()
+const logLoading = ref(false)
+const handleLinkClick = (row) => {
+  logLoading.value = true
+  $api
+    .getAIApiLogDialog({
+      request_id: row['Request ID']
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        logLoading.value = false
+        const data = res.data.Data
+        // 打开日志详情对话框
+        logDialogRef.value.openDialog(data.request_content, data.ai_response_content)
+      }
+    })
+}
+
+defineExpose({
+  SearchOperationLog
+})
+</script>
+
+<template>
+  <div
+    class="table-box"
+    v-loading.fullscreen.lock="exportLoading"
+    element-loading-text="Loading..."
+    element-loading-custom-class="element-loading"
+    element-loading-background="rgb(43, 47, 54, 0.7)"
+  >
+    <vxe-grid
+      ref="tableRef"
+      v-vloading="tableLoadingTableData || tableLoadingColumn"
+      :height="props.height"
+      :style="{ border: 'none' }"
+      v-bind="tableData"
+      @checkbox-change="handleCheckboxChange"
+      @checkbox-all="handleCheckAllChange"
+    >
+      <!-- action操作栏的插槽 -->
+      <template #action="{ row }">
+        <el-button
+          class="el-button--blue"
+          style="height: 24px; padding: 8px 4px; padding-left: 5px; font-size: 12px"
+        >
+          <span
+            style="margin-right: 2px; font-size: 15px"
+            class="font_family icon-icon_edit_b"
+          ></span>
+        </el-button>
+        <el-button
+          class="el-button--blue"
+          style="height: 24px; padding: 8px 4px; padding-left: 5px; font-size: 12px"
+        >
+          <span
+            style="margin-right: 2px; font-size: 15px"
+            class="font_family icon-icon_clone_b"
+          ></span>
+        </el-button>
+        <el-button
+          class="el-button--blue"
+          style="height: 24px; padding: 8px 4px; padding-left: 5px; font-size: 12px"
+        >
+          <span
+            style="margin-right: 2px; font-size: 15px"
+            class="font_family icon-icon_delete_b"
+          ></span>
+        </el-button>
+      </template>
+      <!-- 空数据时的插槽 -->
+      <template #empty v-if="!tableLoadingTableData && tableData.data.length === 0">
+        <div class="empty-box">
+          <el-button class="el-button--dark">
+            <span class="font_family icon-icon_add_b"></span> Create New Report Template</el-button
+          >
+          <p>Click the "Create New Report Template" button to add a report template.</p>
+        </div>
+      </template>
+      <template #link="{ row, column }">
+        <span style="color: var(--color-theme); cursor: pointer" @click="handleLinkClick(row)">
+          {{ row[column.field] }}
+        </span>
+      </template>
+    </vxe-grid>
+    <vxe-grid :height="10" ref="allTableRef" class="all-table" v-bind="allTable"> </vxe-grid>
+    <div class="bottom-pagination">
+      <div class="left-total-records">Total {{ formatNumber(pageInfo.total) }}</div>
+      <div class="right-pagination">
+        <el-pagination
+          v-model:current-page="pageInfo.pageNo"
+          v-model:page-size="pageInfo.pageSize"
+          :page-sizes="[20, 50, 100, 150]"
+          :pager-count="3"
+          background
+          layout="sizes, prev, pager, next"
+          :total="pageInfo.total"
+          @size-change="getTableData(true)"
+          @current-change="getTableData(true)"
+        />
+      </div>
+    </div>
+    <DownloadDialog
+      @export="getExportTableData"
+      :isHideSelectColumn="true"
+      ref="downloadDialogRef"
+    />
+    <LogDialog ref="logDialogRef" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.table-tools {
+  position: relative;
+  display: flex;
+  justify-content: space-between;
+  height: 48px;
+  padding: 8px 0;
+
+  .left-total-records {
+    font-size: 16px;
+    font-weight: 700;
+    line-height: 32px;
+  }
+}
+
+.bottom-pagination {
+  display: flex;
+  justify-content: space-between;
+  height: 40px;
+  margin-top: -1px;
+  padding: 4px 8px;
+  border: 1px solid var(--color-border);
+  border-radius: 0 0 12px 12px;
+
+  .left-total-records {
+    line-height: 32px;
+  }
+
+  .right-pagination {
+    display: flex;
+    align-items: center;
+  }
+}
+.table-box {
+  padding: 8px 20px 0;
+  position: relative;
+  overflow: hidden;
+
+  .all-table {
+    position: absolute;
+    top: -100000px;
+    width: 20px;
+  }
+  .empty-box {
+    text-align: center;
+    p {
+      color: var(--color-neutral-2);
+      margin-top: 12px;
+    }
+  }
+}
+</style>

+ 196 - 0
src/views/TemplateManagement/src/components/TableView/src/components/DownloadDialog.vue

@@ -0,0 +1,196 @@
+<script setup lang="ts">
+const props = withDefaults(
+  defineProps<{
+    isHideSelectColumn: boolean
+  }>(),
+  { isHideSelectColumn: false }
+)
+const dialogVisible = ref(false)
+
+const openDialog = (selectedColumns: string[], slectedDataNumber: number) => {
+  selectedDataNumber.value = slectedDataNumber
+  columns.value = selectedColumns
+  dialogVisible.value = true
+}
+
+const isShowSelectColumn = ref(false)
+
+const downloadFilter = ref(1)
+const selectedDataNumber = ref(0)
+
+const columns = ref()
+
+const emits = defineEmits<{ export: [number] }>()
+const handleDownload = () => {
+  emits('export', downloadFilter.value)
+}
+
+const clearData = () => {
+  isShowSelectColumn.value = false
+  downloadFilter.value = 1
+}
+
+defineExpose({
+  openDialog,
+  handleDownload
+})
+</script>
+
+<template>
+  <div>
+    <el-dialog @close="clearData" v-model="dialogVisible" title="Download File" width="540">
+      <div class="download-dialog">
+        <div class="select-data">
+          <div style="display: inline-block">
+            Select data on your Opeartion Log list:<span style="color: var(--color-theme)">{{
+              selectedDataNumber
+            }}</span>
+          </div>
+        </div>
+        <div class="download-filter" v-if="!props.isHideSelectColumn">
+          <el-radio-group v-model="downloadFilter">
+            <el-radio :value="1"
+              >Download with selected columns
+              <span class="column-number">{{ columns.length }}</span>
+              <SeeAllIcon v-model="isShowSelectColumn" />
+            </el-radio>
+            <div
+              v-if="isShowSelectColumn"
+              class="select-columns"
+              :class="{ show: isShowSelectColumn }"
+            >
+              <div class="title">Selected columns</div>
+              <div class="content">
+                <div class="column-item" v-for="item in columns" :key="item">{{ item }}</div>
+              </div>
+            </div>
+            <el-radio :value="2">Download with all columns</el-radio>
+          </el-radio-group>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button class="cancel-btn" type="default" @click="dialogVisible = false"
+            >Cancel</el-button
+          >
+          <el-button class="download-btn el-button--dark" @click="handleDownload"
+            ><span style="margin-right: 8px" class="font_family icon-icon_download_b"></span>
+            Download</el-button
+          >
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.download-dialog {
+  color: var(--color-neutral-1);
+}
+
+.select-data {
+  font-weight: 700;
+}
+
+.data-filter {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  max-height: 120px;
+  margin-top: 8px;
+  overflow: auto;
+
+  .filter-item {
+    height: 22px;
+    padding: 0px 8px;
+    background-color: var(--color-download-file-filter-tag-bg);
+    border-radius: 12px;
+    line-height: 22px;
+    font-size: 12px;
+  }
+}
+
+.download-filter {
+  margin-top: 16px;
+
+  .el-radio-group {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+
+    .el-radio {
+      height: 40px;
+      align-items: center;
+    }
+
+    :deep(.el-radio__label) {
+      margin-top: 2px;
+      font-weight: 700;
+      color: var(--color-neutral-1);
+    }
+
+    .column-number {
+      padding: 3px 5px;
+      background-color: var(--color-theme);
+      border-radius: 12px;
+      font-size: 12px;
+      font-weight: 700;
+      color: #fff;
+    }
+
+    .see-all-btn {
+      margin-left: 8px;
+      color: var(--color-theme);
+      font-size: 12px;
+    }
+
+    .select-columns {
+      max-height: 350px;
+      padding: 8px;
+      margin-top: 8px;
+      background-color: var(--color-dialog-header-bg);
+      border-radius: 6px;
+      overflow: hidden;
+
+      &.show {
+        max-height: 500px;
+      }
+
+      .title {
+        font-size: 12px;
+        font-weight: 700;
+      }
+
+      .content {
+        display: flex;
+        flex-wrap: wrap;
+        margin-top: 8px;
+        gap: 8px;
+
+        .column-item {
+          height: 22px;
+          padding: 0px 8px;
+          background-color: var(--color-download-file-selected-column-tag-bg);
+          line-height: 22px;
+          border-radius: 12px;
+          font-size: 12px;
+        }
+      }
+    }
+  }
+}
+
+.dialog-footer {
+  .el-button {
+    height: 40px;
+  }
+
+  .cancel-btn {
+    width: 115px;
+  }
+
+  .download-btn {
+    width: 136px;
+  }
+}
+</style>