Explorar o código

Merge branch 'dev' into dev_g

AmandaG hai 6 meses
pai
achega
ef44ba6fd5

+ 61 - 1
src/api/module/AIRobot.ts

@@ -57,4 +57,64 @@ export const PromptAITest = (params: any, config: any) => {
     },
     config
   )
-}
+}
+
+/**
+ * 获取prompt提示器
+ */
+export const getPrompt = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat',
+      operate: 'ai_chat_prompt',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * ai聊天
+ */
+export const aiChat = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat',
+      operate: 'ai_chat',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 暂停ai聊天
+ */
+export const pauseAiChat = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat',
+      operate: 'ai_chat_stop',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 评价ai聊天
+ */
+export const feedbackAiChat = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat',
+      operate: 'ai_chat_answer_mark',
+      ...params
+    },
+    config
+  )
+}

+ 30 - 0
src/api/module/tracking.ts

@@ -78,6 +78,36 @@ export const getTrackingAmsIsf = (params: any, config: any) => {
   )
 }
 
+/**
+ * 获取 vgm 的默认配置
+ */
+export const getVGMDefaultSet = (params: any, config: any) => {
+  return HttpAxios.get(
+    `${baseUrl}`,
+    {
+      action: 'ocean_order',
+      operate: 'default_vgm',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 保存 vgm 的默认配置
+ */
+export const saveVGMDefaultSet = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'ocean_order',
+      operate: 'default_vgm_update',
+      ...params
+    },
+    config
+  )
+}
+
 /**
  * 获取add vgm页面数据
  */

+ 2 - 2
src/components/NotificationMessageCard/src/components/EventCard.vue

@@ -158,7 +158,7 @@ const jumpTracking = (data: EventCardPropsData) => {
         <span v-else class="font_family icon-icon_time_b"></span>
         <span style="margin-right: 2px" v-if="data.info.timeLabel">{{ data.info.timeLabel }}:</span>
         <span style="margin-right: 3px">{{
-          dayjs(data.info.time).format('MMM DD, YYYY hh:mm')
+          dayjs(data.info.time).format('MMM DD, YYYY HH:mm')
         }}</span>
         <span>{{ getTimezone(data.info.timezone, data.info.time) }}</span>
       </div>
@@ -169,7 +169,7 @@ const jumpTracking = (data: EventCardPropsData) => {
       <div class="time" :class="{ grey: data.type === 'delay' || data.type === 'change' }">
         <span class="font_family icon-icon_time_b"></span>
         <span style="margin-right: 3px" v-if="data.timeLabel">{{ data.timeLabel }}:</span>
-        <span style="margin-right: 3px">{{ dayjs(data.time).format('MMM DD, YYYY hh:mm') }}</span>
+        <span style="margin-right: 3px">{{ dayjs(data.time).format('MMM DD, YYYY HH:mm') }}</span>
         <span>{{ getTimezone(data.timezone, data.time) }}</span>
       </div>
       <div class="previous" v-if="data.previous">

+ 13 - 4
src/styles/reset.scss

@@ -136,6 +136,8 @@ div {
 }
 
 .markdown-body {
+  display: inline-block;
+  width: 100%;
   line-height: 1.6;
   font-size: 16px;
   color: #333;
@@ -199,12 +201,19 @@ div {
   overflow-x: auto;
 }
 
-.markdown-test b, .markdown-test strong,.markdown-test p,.markdown-test code,.markdown-test blockquote,.markdown-test ul,
-.markdown-test ol,.markdown-test li,.markdown-test h1,
+.markdown-test b,
+.markdown-test strong,
+.markdown-test p,
+.markdown-test code,
+.markdown-test blockquote,
+.markdown-test ul,
+.markdown-test ol,
+.markdown-test li,
+.markdown-test h1,
 .markdown-test h2,
 .markdown-test h3,
 .markdown-test h4,
 .markdown-test h5,
 .markdown-test h6 {
-  color: #FFF;
-}
+  color: #fff;
+}

+ 9 - 3
src/utils/tools.ts

@@ -6,11 +6,17 @@ const formatString = computed(() => {
   return userStore.dateFormat || 'MM/DD/YYYY'
 })
 
-export const formatTimezone = (time: string, timezone?: string) => {
+export const formatTimezone = (time: string, timezone?: string, is12HourClock?: boolean) => {
   if (!time) return '--'
   let formattedTime = ''
   if (time.length > 12) {
-    formattedTime = moment(time).format(`${formatString.value} hh:mm A`)
+    if (is12HourClock) {
+      // 如果是12小时制,使用12小时制格式化
+      formattedTime = moment(time).format(`${formatString.value} hh:mm A`)
+    } else {
+      // 如果是24小时制,使用24小时制格式化
+      formattedTime = moment(time).format(`${formatString.value} HH:mm`)
+    }
     if (!timezone) {
       return formattedTime
     }
@@ -41,7 +47,7 @@ export const getTimezone = (timezone: string, time?: string): string => {
 export const formatTimezoneByUTCorGMT = (time: string, timezone: string) => {
   if (!time) return '--'
   let formattedTime = ''
-  formattedTime = moment(time).format(`${formatString.value} hh:mm A`)
+  formattedTime = moment(time).format(`${formatString.value} HH:mm`)
   let gmtOffset = ''
   if (timezone != null) {
     const timeZoneOffset = moment.tz(time, timezone).format('Z')

+ 122 - 70
src/views/AIRobotChat/src/AIRobotChat.vue

@@ -7,9 +7,11 @@ import userBubbleDark from './image/userBubbleDark.png'
 import robotBubbleLight from './image/robotBubbleLight.png'
 import robotBubbleDark from './image/robotBubbleDark.png'
 import { useThemeStore } from '@/stores/modules/theme'
+import { useUserStore } from '@/stores/modules/user'
 import MarkdownIt from 'markdown-it'
 import 'github-markdown-css/github-markdown.css'
 
+const userStore = useUserStore()
 const md = new MarkdownIt({
   html: true,
   linkify: true,
@@ -18,6 +20,10 @@ const md = new MarkdownIt({
 })
 
 const renderedMessage = (content) => {
+  // console.log('content', content)
+  if (!content) {
+    return ''
+  }
   const normalized = content
     .replace(/\\n/g, '\n') // 粘贴带来的 \\n 处理
     .replace(/\r\n/g, '\n') // Windows 换行标准化
@@ -45,36 +51,21 @@ const isShowHeaderShadow = ref(false)
 
 const loadingAnswer = ref(false) // 是否正在加载答案
 interface MessageItem {
+  id?: string // 唯一标识
   type: 'robot' | 'user'
   content: string
-  feedback?: 'good' | 'noGood' | '' // 反馈结果
+  feedback?: 'Cood' | 'Not Good' | '' // 反馈结果
   isShowFeedback?: boolean // 是否展示反馈样式
   isAnswer?: boolean // 是否为用户问题的答案,是则才能展示反馈组件
   isError?: boolean // 是否为错误消息
   isCancel?: boolean // 是否为取消消息
+  html?: string // 渲染后的HTML内容
 }
 const messages = ref<MessageItem[]>([
   {
     type: 'robot',
     isShowFeedback: false,
     content: 'You can click on Frequently Asked Questions above or type your own question'
-  },
-  {
-    type: 'user',
-    content:
-      'Hi! I am your Freight Assistant. How can I help you?Of course! Please provide me with the details of your shipment.Of course! Please provide me with the details of your shipment.'
-  },
-  {
-    type: 'robot',
-    feedback: '',
-    isShowFeedback: false,
-    isAnswer: true,
-    content: 'Can you help me with my shipment?'
-  },
-  {
-    type: 'user',
-    content:
-      'Of course! Please provide me with the details of your shipment.Of course! Please provide me with the details of your shipment.'
   }
 ])
 messages.value = JSON.parse(sessionStorage.getItem('AIChat')) || messages.value
@@ -106,10 +97,75 @@ const progressStatus = {
 
 const isShowTips = ref(false) // 是否展示提示信息
 
+const parseHtmlString = (data) => {
+  const lines = data.split('\n')
+
+  function streamMarkdown() {
+    const lastMsg: any = messages.value[messages.value.length - 1]
+    let index = 0
+    lastMsg.content = '' // 清空上一个消息的内容
+    const timer = setInterval(() => {
+      if (index < lines.length) {
+        lastMsg.content += lines[index] + '\n'
+        lastMsg.html = renderedMessage(lastMsg.content) // ✅ 每次整段渲染
+        index++
+        scrollToBottom() // 滚动到底部
+      } else {
+        clearInterval(timer)
+      }
+    }, 150)
+  }
+
+  streamMarkdown()
+}
+
 const progressInterval = ref()
-const handleSend = (question, isPresetQuestion = true) => {
-  if (!question) return
+const serial_no = ref()
+const aiChat = (question, isPresetQuestion) => {
+  serial_no.value = userStore.userInfo?.uname + Date.now().toString()
+  $api
+    .aiChat({
+      serial_no: serial_no.value,
+      prompt: sessionStorage.getItem('prompt'),
+      // question_type: isPresetQuestion ? 'Predefined Question' : 'Free Question',
+      question_type: 'Free Question',
+      question_content: question
+    })
+    .then((res) => {
+      if (isPause.value) {
+        return
+      }
+      if (res.code === 200) {
+        clearInterval(progressInterval.value)
+        const { data } = res.data
+        messages.value[messages.value.length - 1] = {
+          id: serial_no.value,
+          type: 'robot',
+          content: '',
+          feedback: '',
+          isShowFeedback: false,
+          isAnswer: true
+        }
 
+        parseHtmlString(data)
+        scrollToBottom()
+        loadingAnswer.value = false
+        queryTime.value = -1
+      } else {
+        loadingAnswer.value = false
+        messages.value[messages.value.length - 1].isError = true
+      }
+    })
+}
+const handleSend = (question, isPresetQuestion = true, isExternal = false) => {
+  if (!question) return
+  if (
+    isExternal &&
+    messages.value.length === 1 &&
+    messages.value[0].content === progressStatus.init
+  ) {
+    messages.value.pop()
+  }
   if (loadingAnswer.value) {
     isShowTips.value = true
     setTimeout(() => {
@@ -117,8 +173,11 @@ const handleSend = (question, isPresetQuestion = true) => {
     }, 2000)
     return
   }
+  isPause.value = false
   loadingAnswer.value = true
 
+  aiChat(question, isPresetQuestion)
+  // 将用户内容添加到消息列表
   messages.value.push({
     type: 'user',
     content: question
@@ -181,45 +240,40 @@ function scrollToBottom() {
   })
 }
 
-const simulateStreamingMarkdown = () => {
-  loadingAnswer.value = true
-  const chunks: any = [userQuestion.value]
-
-  messages.value.push({
-    type: 'robot',
-    content: '',
-    isAnswer: true,
-    feedback: '',
-    isShowFeedback: false
-  })
-
-  let index = 0
-
-  autoScroll.value = true
-  scrollToBottom()
-
-  const interval = setInterval(() => {
-    index === chunks.length - 1 ? (loadingAnswer.value = false) : ''
-    if (index < chunks.length) {
-      messages.value[messages.value.length - 1].content += chunks[index]
-      index++
-
-      if (autoScroll.value) {
+const isPause = ref(false) // 是否暂停回答
+// 暂停回答
+const handlePause = () => {
+  $api
+    .pauseAiChat({
+      serial_no: serial_no.value
+    })
+    .then((res) => {
+      if (res.code === 200) {
+        clearInterval(progressInterval.value)
+        queryTime.value = -1
+        messages.value[messages.value.length - 1].isCancel = true
+        messages.value[messages.value.length - 1].content = progressStatus.cancel
+        isPause.value = true
+        loadingAnswer.value = false
         scrollToBottom()
       }
-    } else {
-      clearInterval(interval)
-      userQuestion.value = ''
-    }
-  }, 200) // 每500ms追加一段
+    })
 }
 
-// 暂停回答
-const handlePause = () => {
-  clearInterval(progressInterval.value)
-  queryTime.value = -1
-  messages.value[messages.value.length - 1].isCancel = true
-  messages.value[messages.value.length - 1].content = progressStatus.cancel
+// 评价
+const handleFeedback = (index, feedback) => {
+  const message = messages.value[index]
+  message.feedback = feedback
+  $api
+    .feedbackAiChat({
+      serial_no: message.id,
+      feedback: feedback
+    })
+    .then((res) => {
+      if (res.code === 200) {
+        messages.value[index].isShowFeedback = false
+      }
+    })
 }
 
 const emit = defineEmits(['close'])
@@ -289,37 +343,35 @@ defineExpose({
           src="./image/icon_loading.png"
           alt=""
         />
-        <!-- <span v-if="!msg.isAnswer">{{ msg.content }}</span> -->
-        <div>
-          <div v-html="renderedMessage(msg.content)" class="markdown-body"></div>
-          <LoadingDots
-            v-if="index === messages.length - 1 && msg.isAnswer && loadingAnswer"
-          ></LoadingDots>
-          <div></div>
+        <div style="display: inline-block; max-width: 100%">
+          <div v-html="msg.html || renderedMessage(msg.content)" class="markdown-body"></div>
         </div>
+        <LoadingDots
+          v-if="index === messages.length - 1 && msg.isAnswer && loadingAnswer"
+        ></LoadingDots>
         <!-- 评价  -->
         <div class="review" v-if="msg.isShowFeedback && msg.isAnswer">
           <el-button
-            v-if="msg.feedback !== 'good'"
+            v-if="msg.feedback !== 'Cood'"
             class="el-button--text"
-            @click="msg.feedback = 'good'"
+            @click="handleFeedback(index, 'Cood')"
           >
             <span class="font_family icon-icon_good_b"></span>
           </el-button>
-          <div v-if="msg.feedback === 'good'" style="width: 16px; text-align: center">
+          <div v-if="msg.feedback === 'Cood'" style="width: 16px; text-align: center">
             <span
               style="color: var(--color-theme); font-size: 14px"
               class="font_family icon-icon_good__filled_b"
             ></span>
           </div>
           <el-button
-            v-if="msg.feedback !== 'noGood'"
+            v-if="msg.feedback !== 'Not Good'"
             class="el-button--text"
-            @click="msg.feedback = 'noGood'"
+            @click="handleFeedback(index, 'Not Good')"
           >
             <span class="font_family icon-icon_notgood_b"></span>
           </el-button>
-          <div v-if="msg.feedback === 'noGood'" style="width: 16px; text-align: center">
+          <div v-if="msg.feedback === 'Not Good'" style="width: 16px; text-align: center">
             <span
               style="color: var(--color-theme); font-size: 14px"
               class="font_family icon-icon_notgood__filled_b"
@@ -356,7 +408,6 @@ defineExpose({
           @focus="isFooterInputFocus = true"
           @blur="isFooterInputFocus = false"
         />
-        <el-button @click="simulateStreamingMarkdown">测试</el-button>
         <div
           class="input-icon"
           :class="[!userQuestion || queryTime !== -1 ? 'disable' : '']"
@@ -526,7 +577,8 @@ defineExpose({
       .pause-btn {
         position: absolute;
         right: -22px;
-        top: 13px;
+        top: 50%;
+        transform: translateY(-50%);
         display: flex;
         align-items: center;
         justify-content: center;

+ 13 - 4
src/views/Layout/src/LayoutView.vue

@@ -19,9 +19,7 @@ const isShowAIRobotChat = ref(false)
 const handleColseRobotChat = () => {
   isShowAIRobotChat.value = false
 }
-const onClick = () => {
-  isShowAIRobotChat.value = true
-}
+
 const AvatarClick = () => {
   isShowAIRobotChat.value = true
 }
@@ -32,9 +30,20 @@ const handelClickAIDefault = async (item: any) => {
   isShowAIRobotChat.value = true
   await nextTick()
   if (AIRobotChatref.value) {
-    AIRobotChatref.value.handleSend(item)
+    AIRobotChatref.value.handleSend(item, true, true)
   }
 }
+
+const getPrompt = () => {
+  $api.getPrompt().then((res) => {
+    if (res.code === 200) {
+      sessionStorage.setItem('prompt', JSON.stringify(res.data.prompt))
+    }
+  })
+}
+onMounted(() => {
+  getPrompt()
+})
 </script>
 <template>
   <el-container class="layout-container">

+ 157 - 0
src/views/Tracking/src/components/TrackingTable/src/components/DefaultSettingDialog.vue

@@ -0,0 +1,157 @@
+<script setup lang="ts">
+import { cloneDeep } from 'lodash'
+
+const openDialog = () => {
+  dialogVisible.value = true
+}
+
+const dialogVisible = ref(false)
+const formData = ref({
+  submitter: '',
+  signature: '',
+  authorized_email: '',
+  authorized_tel: ''
+})
+
+const defaultSetting = ref()
+const getDefaultSettings = () => {
+  $api
+    .getVGMDefaultSet()
+    .then((res) => {
+      formData.value = res.data.vgm
+      defaultSetting.value = cloneDeep(res.data.vgm)
+    })
+    .catch(() => {})
+}
+getDefaultSettings()
+
+const handleSave = () => {
+  $api
+    .saveVGMDefaultSet(formData.value)
+    .then(() => {
+      defaultSetting.value = cloneDeep(formData.value)
+      ElMessage.success('Settings saved successfully')
+    })
+    .catch(() => {})
+}
+
+const emit = defineEmits(['close'])
+const clearData = () => {
+  formData.value = cloneDeep(defaultSetting.value) || {
+    submitter: '',
+    signature: '',
+    authorized_email: '',
+    authorized_tel: ''
+  }
+  emit('close', cloneDeep(defaultSetting.value))
+}
+
+defineExpose({
+  openDialog
+})
+</script>
+
+<template>
+  <el-dialog
+    v-model="dialogVisible"
+    title="Default Settings"
+    width="50%"
+    @closed="clearData"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+  >
+    <div class="form">
+      <div class="form-row">
+        <div class="form-item">
+          <div class="label">Submitter</div>
+          <div class="content">
+            <el-input
+              v-model="formData.submitter"
+              placeholder="Please enter..."
+              clearable
+            ></el-input>
+          </div>
+        </div>
+        <div class="form-item">
+          <div class="label">Signature</div>
+          <div class="content">
+            <el-input
+              v-model="formData.signature"
+              placeholder="Please enter..."
+              clearable
+            ></el-input>
+          </div>
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-item">
+          <div class="label">Authorized Email</div>
+          <div class="content">
+            <el-input
+              v-model="formData.authorized_email"
+              placeholder="Please enter..."
+              clearable
+            ></el-input>
+          </div>
+        </div>
+        <div class="form-item">
+          <div class="label">Authorized Tel</div>
+          <div class="content">
+            <el-input
+              v-model="formData.authorized_tel"
+              placeholder="Please enter..."
+              clearable
+            ></el-input>
+          </div>
+        </div>
+      </div>
+    </div>
+    <template #footer>
+      <el-button style="width: 81px" class="el-button--default" @click="dialogVisible = false"
+        >Cancel</el-button
+      >
+      <el-button @click="handleSave" class="el-button--dark" style="width: 86px">Save</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<style lang="scss" scoped>
+.form {
+  position: relative;
+  .form-row {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 16px;
+    margin-bottom: 16px;
+    .form-item {
+      flex: 1;
+      .label {
+        display: flex;
+        align-items: flex-start;
+        margin-bottom: 8px;
+        font-size: 12px;
+        line-height: 16px;
+        color: var(--dashboard-text-color);
+      }
+      .content {
+        display: flex;
+        align-items: center;
+        height: 32px;
+        .el-input {
+          width: 100%;
+        }
+      }
+    }
+  }
+  .default-setting {
+    position: absolute;
+    top: 8px;
+    right: 8px;
+    height: 21px;
+    line-height: 21px;
+    color: var(--color-theme);
+    border-bottom: 1px solid var(--color-theme);
+    cursor: pointer;
+  }
+}
+</style>

+ 41 - 6
src/views/Tracking/src/components/TrackingTable/src/components/VGMView.vue

@@ -5,6 +5,7 @@ import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
 import { formatTimezone } from '@/utils/tools'
 import { useUserStore } from '@/stores/modules/user'
 import dayjs from 'dayjs'
+import DefaultSettingDialog from './DefaultSettingDialog.vue'
 
 const userStore = useUserStore()
 const datePickerFormat = `${userStore.dateFormat} HH:mm:ss`
@@ -50,7 +51,7 @@ const generalInfo = ref({
     }
   ],
   formData: {
-    Submitter: '',
+    submitter: '',
     signature: '',
     authorized_email: '',
     authorized_tel: '',
@@ -199,7 +200,7 @@ const convertData = (data: any) => {
       }
     ],
     formData: {
-      Submitter: data.Submitter,
+      submitter: data.submitter,
       signature: data.signature,
       authorized_email: data.authorized_email,
       authorized_tel: data.authorized_tel,
@@ -269,7 +270,7 @@ const verificationData = () => {
       fieldValue === null || fieldValue === undefined || fieldValue === ''
   }
 
-  checkField('Submitter', 'submitter')
+  checkField('submitter', 'submitter')
   checkField('signature', 'signature')
   checkField('authorized_email', 'authorized_email')
   checkField('authorized_tel', 'authorized_tel')
@@ -305,7 +306,7 @@ const handleSave = () => {
 
   const generalData = {
     all_carrier_booking: generalInfo.value.baseInfo['Carrier Booking No.'],
-    submitter: generalInfo.value.formData.Submitter,
+    submitter: generalInfo.value.formData.submitter,
     signature: generalInfo.value.formData.signature,
     authorized_email: generalInfo.value.formData.authorized_email,
     authorized_tel: generalInfo.value.formData.authorized_tel,
@@ -365,6 +366,24 @@ const stopScroll = (evt) => {
   }
   return false
 }
+
+const defaultSettingDialogRef = ref<InstanceType<typeof DefaultSettingDialog> | null>(null)
+const openDefaultSettingDialog = () => {
+  if (defaultSettingDialogRef.value) {
+    defaultSettingDialogRef.value.openDialog()
+  }
+}
+const handleDefaultSetting = (data: any) => {
+  // debugger
+  // generalInfo.value.formData = data
+  const formData = generalInfo.value.formData
+  for (const key in formData) {
+    if (!formData[key] && data[key]) {
+      formData[key] = data[key]
+      isVerificationError.value[key] = false
+    }
+  }
+}
 </script>
 
 <template>
@@ -406,7 +425,7 @@ const stopScroll = (evt) => {
               <div class="content">
                 <el-input
                   :class="{ 'is-error': isVerificationError.submitter }"
-                  v-model="generalInfo.formData.Submitter"
+                  v-model="generalInfo.formData.submitter"
                   placeholder="Please enter..."
                   clearable
                   @blur="verificationData"
@@ -473,6 +492,7 @@ const stopScroll = (evt) => {
               </div>
             </div>
           </div>
+          <div class="default-setting" @click="openDefaultSettingDialog">Default Setting</div>
         </div>
       </div>
       <div class="detail-info" style="margin-top: 8px">
@@ -516,7 +536,7 @@ const stopScroll = (evt) => {
                 style="width: 190px"
                 placeholder="Pick a Date"
                 :format="datePickerFormat"
-                value-format="YYYY-MM-DD hh:mm:ss"
+                value-format="YYYY-MM-DD HH:mm:ss"
                 :date-format="userStore.dateFormat"
                 time-format="HH:mm:ss"
               />
@@ -528,6 +548,10 @@ const stopScroll = (evt) => {
         </div>
       </div>
     </div>
+    <DefaultSettingDialog
+      @close="handleDefaultSetting"
+      ref="defaultSettingDialogRef"
+    ></DefaultSettingDialog>
   </div>
 </template>
 
@@ -613,6 +637,7 @@ const stopScroll = (evt) => {
     padding: 0 16px 8px;
   }
   .form {
+    position: relative;
     .form-row {
       display: flex;
       flex-wrap: wrap;
@@ -650,6 +675,16 @@ const stopScroll = (evt) => {
         }
       }
     }
+    .default-setting {
+      position: absolute;
+      top: 8px;
+      right: 8px;
+      height: 21px;
+      line-height: 21px;
+      color: var(--color-theme);
+      border-bottom: 1px solid var(--color-theme);
+      cursor: pointer;
+    }
   }
 }
 .data-info {