Преглед изворни кода

feat: 实现Destination Delivery部分页面样式

zhouyuhao пре 6 месеци
родитељ
комит
6d18cbadbf

+ 10 - 1
src/components/VTag/src/VTag.vue

@@ -10,6 +10,7 @@ interface internalProps {
     | 'Arrived'
     | 'Completed'
     | 'Departed'
+    | 'Pending Approval'
   large?: boolean
 }
 
@@ -22,7 +23,8 @@ const mappingTable = new Map([
   ['Departure', 'departure'],
   ['Arrived', 'arrived'],
   ['Completed', 'completed'],
-  ['Departed', 'Departed']
+  ['Departed', 'Departed'],
+  ['Pending Approval', 'pending-approval']
 ])
 defineProps<internalProps>()
 </script>
@@ -122,6 +124,13 @@ defineProps<internalProps>()
       background-color: var(--color-tag-Departed);
     }
   }
+  &.v-tag__pending-approval {
+    background-color: var(--color-tag-pending-approval-bg);
+    color: var(--color-tag-pending-approval);
+    .dot {
+      background-color: var(--color-tag-pending-approval);
+    }
+  }
   & + .v-tag {
     margin-left: 8px;
   }

+ 14 - 0
src/styles/theme.scss

@@ -72,6 +72,8 @@
   --color-tag-arrived-bg: #e7faf8;
   --color-tag-completed-bg: #e8fbe4;
   --color-tag-Departed-bg: #d9edfa;
+  --color-tag-unfinished-approval-bg: #fbfbfe;
+  --color-tag-unfinished-approval: #e0a100;
 
   --color-border: #eaebed;
   --color-select-border: #eaebed;
@@ -319,6 +321,11 @@
 
   --color-card-icon-box-bg: #f0f1fb;
   --color-card-number-cancelled: #243041;
+  --color-steps-unfinished-line: #b5b9bf;
+  --color-steps-current-icon-color: #e0a100;
+  --color-steps-current-icon-bg: #fff4d1;
+  --color-booking-info-linear-bg: linear-gradient(90deg, #c4c9ee 0%, #e8e8ff 49.52%, #bfe1ff 100%);
+  --color-process-data-value-bg: #e8ebef;
 }
 
 :root.dark {
@@ -513,6 +520,13 @@
     }
   }
 
+  --color-tag-unfinished-approval-bg: #eeeff6;
+
   --color-card-icon-box-bg: #4f535c;
   --color-card-number-cancelled: #babcc0;
+  --color-steps-unfinished-line: #b5b9bf;
+  --color-steps-current-icon-color: #e0a100;
+  --color-steps-current-icon-bg: #534b30;
+  --color-booking-info-linear-bg: linear-gradient(90deg, #636db7 0%, #515195 49.52%, #7b9bc9 100%);
+  --color-process-data-value-bg: #4f5760;
 }

+ 0 - 89
src/views/DestinationDelivery/src/components/BookingDetailDialog.vue

@@ -1,89 +0,0 @@
-<script setup lang="ts">
-const visible = ref(false)
-
-const openDialog = (row: any) => {
-  visible.value = true
-  console.log(row)
-}
-
-const processList = ref([
-  {
-    id: 1,
-    label: 'Created',
-    time: 'Jun-01-2024',
-    icon: 'icon_submit_b'
-  },
-  {
-    label: 'Pending',
-    time: 'Current',
-    icon: 'icon_time_b'
-  },
-  {
-    label: 'Approved',
-    time: '--',
-    icon: 'icon_confirm_b'
-  }
-])
-
-defineExpose({
-  openDialog
-})
-</script>
-
-<template>
-  <el-dialog title="Booking Detail" v-model="visible" :close-on-click-modal="false" width="800px">
-    <div class="header-process">
-      <div class="process-item" v-for="(item, index) in processList" :key="index">
-        <div class="left-icon-box">
-          <span
-            class="font_family"
-            :class="'icon-' + item.icon"
-            :style="{ transform: item.label === 'Created' ? 'rotate(-65deg)' : 'rotate(0)' }"
-          ></span>
-        </div>
-        <div class="right-info">
-          <div class="process-label">{{ item.label }}</div>
-          <div class="process-time">{{ item.time }}</div>
-        </div>
-      </div>
-    </div>
-  </el-dialog>
-</template>
-
-<style lang="scss" scoped>
-.header-process {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 0 100px 20px;
-  .process-item {
-    display: flex;
-    align-items: center;
-    margin-bottom: 20px;
-    .left-icon-box {
-      width: 32px;
-      height: 32px;
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      background-color: var(--color-neutral-1);
-
-      border-radius: 50%;
-      margin-right: 10px;
-      span {
-        color: var(--color-mode);
-      }
-    }
-    .right-info {
-      .process-label {
-        font-weight: 600;
-        color: var(--color-text);
-      }
-      .process-time {
-        font-size: 12px;
-        color: var(--color-neutral-2);
-      }
-    }
-  }
-}
-</style>

+ 16 - 5
src/views/DestinationDelivery/src/components/TableView/src/TableView.vue

@@ -5,7 +5,8 @@ import { autoWidth } from '@/utils/table'
 import { useRowClickStyle } from '@/hooks/rowClickStyle'
 import dayjs from 'dayjs'
 import { formatTimezone, formatNumber } from '@/utils/tools'
-import BookingDetailDialog from '../../BookingDetailDialog.vue'
+import BookingDetailDialog from './components/BookingDetailDialog.vue'
+import EmailDialog from './components/EmailDialog.vue'
 
 const props = defineProps({
   height: {
@@ -266,6 +267,11 @@ const bookingDetailDiaRef = ref()
 const clickViewBtn = (row: any) => {
   bookingDetailDiaRef.value.openDialog(row)
 }
+
+const emailDialogRef = ref()
+const clickEmailBtn = (row: any) => {
+  emailDialogRef.value.openDialog(row)
+}
 defineExpose({
   SearchOperationLog
 })
@@ -309,6 +315,14 @@ defineExpose({
         >
           <span style="color: 'red'" class="font_family icon-icon_view_b"> </span>
         </el-button>
+        <!-- email -->
+        <el-button
+          @click="clickEmailBtn(row)"
+          class="action-btn el-button--blue"
+          style="height: 24px; width: 24px"
+        >
+          <span style="color: 'red'" class="font_family icon-icon_email_b"> </span>
+        </el-button>
         <!-- edit -->
         <el-button class="action-btn el-button--blue" style="height: 24px; width: 24px">
           <span style="color: 'red'" class="font_family icon-icon_edit_b"> </span>
@@ -321,10 +335,6 @@ defineExpose({
         <el-button class="action-btn el-button--blue" style="height: 24px; width: 24px">
           <span style="color: 'red'" class="font_family icon-icon_reject_b"> </span>
         </el-button>
-        <!-- email -->
-        <el-button class="action-btn el-button--blue" style="height: 24px; width: 24px">
-          <span style="color: 'red'" class="font_family icon-icon_email_b"> </span>
-        </el-button>
       </template>
     </vxe-grid>
 
@@ -347,6 +357,7 @@ defineExpose({
 
     <CustomizeColumns @customize="customizeColumns" ref="CustomizeColumnsRef" />
     <BookingDetailDialog ref="bookingDetailDiaRef" />
+    <EmailDialog ref="emailDialogRef" />
   </div>
 </template>
 

+ 169 - 0
src/views/DestinationDelivery/src/components/TableView/src/components/BookingDetailDialog.vue

@@ -0,0 +1,169 @@
+<script setup lang="ts">
+import DetailStep from './DetailStep.vue'
+import ShipmentInforTable from './ShipmentInforTable.vue'
+import OperationLogProcess from './OperationLogProcess.vue'
+
+const visible = ref(false)
+
+const openDialog = (row: any) => {
+  visible.value = true
+  console.log(row)
+}
+
+const stepList: any = ref([
+  {
+    id: 1,
+    label: 'Created',
+    date: 'Jun-01-2024',
+    icon: 'icon_submit_b',
+    status: 'completed'
+  },
+  {
+    label: 'Pending',
+    date: 'Current',
+    icon: 'icon_time_b',
+    status: 'current'
+  },
+  {
+    label: 'Approved',
+    date: '--',
+    icon: 'icon_confirm_b',
+    status: 'unfinished'
+  }
+])
+
+defineExpose({
+  openDialog
+})
+</script>
+
+<template>
+  <el-dialog
+    title="Booking Detail"
+    class="booking-detail-dialog"
+    v-model="visible"
+    :close-on-click-modal="false"
+    width="800px"
+    top="10vh"
+  >
+    <DetailStep :stepList="stepList" />
+    <div class="booking-info">
+      <div class="booking-no">
+        <span class="no">Booking No.B83131200164</span>
+        <v-tag class="tag" type="Pending Approval">Pending Approval</v-tag>
+      </div>
+      <div class="created-time">Created Time:Jun-01-2024</div>
+    </div>
+    <ShipmentInforTable></ShipmentInforTable>
+    <div class="delivery-information">
+      <div class="label">Delivery Information</div>
+      <div class="delivery-info">
+        <div class="delivery-item inline-flex" style="width: 200px">
+          <span class="item-label">Mode Type</span>
+          <span class="item-value">Shanghai, China</span>
+        </div>
+        <div class="delivery-item inline-flex">
+          <span class="item-label">Delivery Date</span>
+          <span class="item-value">
+            <span class="font_family icon-icon_date_b" style="margin-right: 4px"></span>
+            <span style="margin-top: 1px">Jun-15-2024</span>
+          </span>
+        </div>
+        <div class="delivery-item">
+          <span class="item-label">Delivery Address</span>
+          <span class="item-value">
+            <span class="font_family icon-icon_location_b" style="margin-right: 2px"></span>
+            <span style="margin-top: 1px"
+              >Main Distribution Center,160#BEIJING ROAD, JINGAN District, Shenzhen, China</span
+            >
+          </span>
+        </div>
+        <div class="delivery-item">
+          <span class="item-label">Special Requirements</span>
+          <span class="item-value">
+            <span class="font_family icon-icon_paragraph_b" style="margin-right: 2px"></span>
+            <span style="margin-top: 1px">Tail Lift Required</span>
+          </span>
+        </div>
+      </div>
+    </div>
+    <el-divider style="margin-top: 8px; margin-bottom: 20px" />
+    <OperationLogProcess />
+  </el-dialog>
+</template>
+
+<style lang="scss" scoped>
+.booking-info {
+  margin: 34px 16px 0;
+  padding: 8px 16px;
+  border-radius: 12px;
+  background-image: var(--color-booking-info-linear-bg);
+
+  .booking-no {
+    display: flex;
+    align-items: center;
+    .no {
+      margin-top: 2px;
+      font-size: 18px;
+      font-weight: 700;
+      line-height: 21px;
+    }
+    .tag {
+      margin-left: 8px;
+    }
+  }
+  .created-time {
+    margin-top: 8px;
+    font-size: 12px;
+    color: var(--color-neutral-2);
+  }
+}
+
+.delivery-information {
+  margin: 20px 16px 0;
+  .label {
+    margin-bottom: 14px;
+    font-size: 18px;
+    font-weight: 700;
+  }
+
+  .delivery-info {
+    padding: 12px 8px 20px;
+    border-radius: 12px;
+    background-color: var(--color-share-link-bg);
+
+    .delivery-item {
+      display: flex;
+      flex-direction: column;
+      margin-bottom: 16px;
+
+      .item-label {
+        margin-bottom: 8px;
+        font-size: 12px;
+        color: var(--color-neutral-2);
+      }
+      .item-value {
+        display: flex;
+        align-items: center;
+        font-weight: 700;
+      }
+      &.inline-flex {
+        display: inline-flex;
+      }
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+  }
+}
+</style>
+<style lang="scss">
+div.booking-detail-dialog {
+  height: 80%;
+  .el-dialog__body {
+    padding: 0;
+    height: calc(100% - 48px);
+    overflow: auto;
+  }
+}
+</style>

+ 130 - 0
src/views/DestinationDelivery/src/components/TableView/src/components/DetailStep.vue

@@ -0,0 +1,130 @@
+<script setup lang="ts">
+const props = defineProps<{
+  stepList: Array<{
+    id?: number
+    label: string
+    date?: string
+    icon: string
+    status: 'completed' | 'current' | 'unfinished'
+  }>
+}>()
+</script>
+
+<template>
+  <div class="step-container">
+    <template v-for="(step, index) in props.stepList" :key="index">
+      <!-- 单个步骤 -->
+      <div class="step">
+        <div
+          class="step-icon"
+          :class="{
+            'step-icon-unfinished': step.status === 'unfinished',
+            'step-icon-current': step.status === 'current'
+          }"
+        >
+          <span
+            :style="{ transform: step.label === 'Created' ? 'rotate(-60deg)' : '' }"
+            class="font_family"
+            :class="'icon-' + step.icon"
+          ></span>
+        </div>
+        <div
+          class="step-text"
+          :class="{
+            'step-text-unfinished': step.status === 'unfinished'
+          }"
+        >
+          <div class="step-title">{{ step.label }}</div>
+          <div class="step-date">{{ step.date || '--' }}</div>
+        </div>
+      </div>
+
+      <!-- 连线(非最后一个) -->
+      <div
+        v-if="index < stepList.length - 1"
+        :class="[
+          stepList?.[index + 1]?.status === 'unfinished'
+            ? 'step-line-unfinished'
+            : 'step-line-completed'
+        ]"
+        class="step-line"
+      ></div>
+    </template>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.step-container {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  height: 34px;
+  margin-top: 24px;
+  padding: 0 100px;
+}
+
+.step {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+}
+
+.step-icon {
+  width: 32px;
+  height: 32px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: var(--color-neutral-1);
+  span {
+    color: var(--color-mode);
+    font-size: 16px;
+  }
+  &.step-icon-unfinished {
+    background: var(--color-steps-unfinished-line);
+  }
+
+  &.step-icon-current {
+    background: var(--color-steps-current-icon-bg);
+    span {
+      color: var(--color-steps-current-icon-color);
+    }
+  }
+}
+
+.step-text {
+  margin-left: 8px;
+  text-align: left;
+  white-space: nowrap;
+  &.step-text-unfinished {
+    .step-title,
+    .step-date {
+      color: var(--color-steps-unfinished-line);
+    }
+  }
+}
+
+.step-title {
+  font-weight: 600;
+  color: var(--color-neutral-1);
+}
+
+.step-date {
+  font-size: 12px;
+  color: var(--color-neutral-2);
+}
+
+.step-line {
+  height: 0;
+  flex: 1;
+  align-self: flex-start;
+  margin: 8px 16px 0;
+  &.step-line-completed {
+    border-top: 1px solid var(--color-neutral-1);
+  }
+  &.step-line-unfinished {
+    border-top: 1px dashed var(--color-steps-unfinished-line);
+  }
+}
+</style>

+ 375 - 0
src/views/DestinationDelivery/src/components/TableView/src/components/EmailDialog.vue

@@ -0,0 +1,375 @@
+<script setup lang="ts">
+import '@wangeditor/editor/dist/css/style.css'
+import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
+import { i18nChangeLanguage, DomEditor } from '@wangeditor/editor'
+import { formatTimezone } from '@/utils/tools'
+
+i18nChangeLanguage('en')
+
+const visible = ref(true)
+const openDialog = (row) => {
+  visible.value = true
+}
+const props = defineProps({
+  data: Object
+})
+
+const emailData = ref({
+  email: '',
+  ccEmail: '',
+  serial_no: ''
+})
+
+const emailRecords: any = ref([
+  {
+    name: 'John Doe',
+    content: 'This is a test email content.',
+    creatTime: '2024-06-01T16:25:31Z'
+  }
+])
+watch(
+  () => props.data,
+  (newVal) => {
+    if (newVal) {
+      const email = newVal?.email
+      emailData.value.email = email?.email
+      emailData.value.ccEmail = email?.cc_email
+      emailData.value.serial_no = newVal?.serial_no
+      emailRecords.value = email?.emailRecords
+    }
+  },
+  {
+    immediate: true,
+    deep: true
+  }
+)
+
+const editorRef = shallowRef()
+const mode = ref('default')
+// 内容 HTML
+const valueHtml = ref('')
+
+const toolbarConfig = {
+  excludeKeys: [
+    'headerSelect',
+    'italic',
+    'fontFamily',
+    'lineHeight',
+    'group-more-style', // 排除菜单组,写菜单组 key 的值即可
+    'group-video', // 插入视频
+    'insertTable', // 插入表格
+    'codeBlock', // 代码块
+    'fullScreen' // 全屏
+  ]
+}
+const editorConfig = {
+  MENU_CONF: {
+    uploadImage: {
+      server: '/api/upload',
+      base64LimitSize: 5 * 1024 * 1024
+    }
+  }
+}
+
+// 组件销毁时,也及时销毁编辑器
+onBeforeUnmount(() => {
+  const editor = editorRef.value
+  if (editor == null) return
+  editor.destroy()
+})
+
+const handleCreated = (editor: any) => {
+  editorRef.value = editor // 记录 editor 实例,重要!
+  // 在 nextTick 中获取 toolbar 实例
+  nextTick(() => {
+    const toolbar = DomEditor.getToolbar(editor)
+  })
+}
+
+const editorIconList = [
+  {
+    dataMenuKey: 'blockquote',
+    svgUrl: () => import('@/icons/icon_quotes_b.svg')
+  },
+  {
+    dataMenuKey: 'bold',
+    svgUrl: () => import('@/icons/icon_bold_b.svg')
+  },
+  // {
+  //   dataMenuKey: 'underline',
+  //   svgUrl: () => import('@/icons/icon_underline_b.svg')
+  // },
+  {
+    dataMenuKey: 'undo',
+    svgUrl: () => import('@/icons/icon_revoke__b.svg')
+  },
+  {
+    dataMenuKey: 'redo',
+    svgUrl: () => import('@/icons/icon_redo_b.svg')
+  }
+]
+// Vue 组件生命周期钩子函数
+onMounted(async () => {
+  for (const item of editorIconList) {
+    const svgModule = await item.svgUrl()
+    const svgUrl = svgModule.default // 获取 SVG 文件的 URL
+    replaceSvgByDataKey(item.dataMenuKey, svgUrl)
+  }
+})
+const replaceSvgByDataKey = (dataMenuKey: any, svgUrl: any) => {
+  const observer = new MutationObserver((mutationsList, observer) => {
+    const element = document.querySelector(`[data-menu-key="${dataMenuKey}"]`)
+    if (element) {
+      // 获取 SVG 内容
+      fetch(svgUrl)
+        .then((res) => res.text())
+        .then((svgContent) => {
+          // 查找旧的 SVG 标签
+          const oldSvg = element.querySelector('svg')
+          if (oldSvg) {
+            oldSvg.outerHTML = svgContent // 替换 SVG
+          }
+        })
+      observer.disconnect() // 找到元素后停止观察
+    }
+  })
+
+  observer.observe(document.body, {
+    childList: true,
+    subtree: true
+  })
+}
+
+const handleFocusEditor = () => {
+  editorRef.value.focus()
+}
+
+const sendEmail = () => {
+  const html = editorRef.value.getHtml()
+  const text = editorRef.value.getText()
+  $api
+    .sendEmailApi({
+      action: 'ocean_booking',
+      email: emailData.value.email,
+      communication_cc: emailData.value.ccEmail,
+      serial_no: emailData.value.serial_no,
+      content: html,
+      text
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        ElMessage.success('Email sent successfully')
+        emailRecords.value = res.data.emailRecords
+      }
+    })
+    .catch(() => {
+      ElMessage.error('Failed to send email')
+    })
+}
+
+defineExpose({
+  openDialog
+})
+</script>
+
+<template>
+  <el-dialog
+    title="Booking Detail"
+    class="booking-detail-email-dialog"
+    v-model="visible"
+    :close-on-click-modal="false"
+    width="1000px"
+    top="10vh"
+  >
+    <div class="email-view">
+      <div class="email-path">
+        <span class="font_family icon-icon_email_b" style="font-size: 18px"></span>
+        <span class="label">Communicate with us:&nbsp;</span>
+        <span class="content">{{ emailData.email }}</span>
+      </div>
+      <div class="separated-by">
+        <el-input v-model="emailData.ccEmail">
+          <template #prefix>
+            <div
+              style="
+                display: flex;
+                align-items: center;
+                color: var(--color-neutral-1);
+                cursor: default;
+              "
+            >
+              <span style="font-weight: 600">CC:</span>
+              <el-tooltip
+                class="box-item"
+                effect="dark"
+                content="Separated by;"
+                placement="top-start"
+                :offset="-8"
+              >
+                <span class="font_family icon-icon_tipsfilled_b" style="font-size: 19px"></span>
+              </el-tooltip>
+            </div>
+          </template>
+        </el-input>
+      </div>
+      <div class="text-editor">
+        <Toolbar
+          style="border-bottom: 1px solid #ccc"
+          :editor="editorRef"
+          :defaultConfig="toolbarConfig"
+          :mode="mode"
+        />
+        <Editor
+          v-model="valueHtml"
+          :defaultConfig="editorConfig"
+          :mode="mode"
+          @onCreated="handleCreated"
+          @click="handleFocusEditor"
+        />
+      </div>
+      <div>
+        <el-button
+          @click="sendEmail"
+          class="el-button--dark"
+          style="float: right; margin: 8px 0 14px 0; height: 40px"
+          ><span class="font_family icon-icon_submit_b" style="margin-right: 4px"></span> Send
+          Email</el-button
+        >
+      </div>
+    </div>
+    <el-divider style="margin: 16px 0; border-top-color: var(--color-divider)" />
+    <div class="show-records">
+      <div class="record-item" v-for="(item, index) in emailRecords" :key="index">
+        <div class="header">
+          <div class="avatar">
+            <div>{{ item.name?.slice(0, 1) }}</div>
+          </div>
+          <div class="name">{{ item.name }}</div>
+          <div class="date">{{ formatTimezone(item.creatTime) }}</div>
+        </div>
+        <div class="content">
+          {{ item.content }}
+        </div>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<style lang="scss" scoped>
+.email-view {
+  display: flex;
+  flex-direction: column;
+
+  padding: 16px;
+  padding-bottom: 0;
+  border-radius: 12px;
+  background: var(--color-email-bg);
+
+  .show-records {
+    max-height: 370px;
+    overflow: auto;
+  }
+}
+
+:deep(.w-e-text-container) {
+  min-height: 170px;
+  border-radius: 0 0 6px 6px;
+  p {
+    margin: 0px;
+  }
+}
+
+.email-path {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: 12px;
+  font-weight: 600;
+
+  & > .label {
+    margin-left: 8px;
+    padding-top: 2px;
+    color: var(--color-neutral-1);
+    white-space: nowrap;
+  }
+
+  & > .content {
+    display: inline-block;
+    flex: 1;
+    padding-top: 2px;
+    line-height: 18px;
+    color: var(--color-theme);
+    word-break: break-all;
+  }
+}
+.separated-by {
+  :deep(.el-input__wrapper) {
+    box-shadow: 0 0 0 1px var(--color-email-border) inset;
+  }
+}
+
+.text-editor {
+  margin-top: 16px;
+  border-radius: 6px;
+  border: 1px solid var(--color-email-border);
+  // overflow: hidden;
+  :deep(div.w-e-toolbar) {
+    border-radius: 6px 6px 0 0;
+  }
+}
+
+.record-item {
+  margin-top: 16px;
+
+  & > .header {
+    display: flex;
+    align-items: center;
+    padding: 0px 16px 8px;
+    padding-left: 0px;
+
+    .avatar {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      width: 24px;
+      height: 24px;
+      text-align: center;
+      border-radius: 50%;
+      background-color: var(--color-theme);
+      div {
+        height: 14px;
+        line-height: 14px;
+        color: var(--color-avatar);
+        font-weight: 700;
+      }
+    }
+
+    .name {
+      height: 24px;
+      margin-left: 4px;
+      margin-right: 8px;
+      line-height: 24px;
+      font-weight: 700;
+      color: var(--color-neutral-1);
+    }
+
+    .date {
+      height: 24px;
+      line-height: 24px;
+      font-size: 12px;
+      color: var(--color-neutral-2);
+    }
+  }
+
+  & > .content {
+    padding: 16px 6px;
+    background-color: var(--color-share-link-bg);
+    border-radius: 6px;
+  }
+}
+
+:deep(.text-editor) {
+  & > div:first-of-type {
+    border-bottom: 1px solid var(--color-email-border) !important;
+  }
+}
+</style>

+ 127 - 0
src/views/DestinationDelivery/src/components/TableView/src/components/OperationLogProcess.vue

@@ -0,0 +1,127 @@
+<script setup lang="ts">
+const processList = ref([
+  {
+    time: 'Jun-01-2024 16:25:31 UTC+3',
+    label: 'Submit Booking',
+    createdBy: 'Customer A',
+    tips: '--'
+  },
+  {
+    time: 'May-15-2024 16:25:31 UTC+3',
+    label: 'Reject Booking',
+    createdBy: 'John Doe',
+    tips: 'Too early'
+  }
+])
+</script>
+
+<template>
+  <div class="operation-log">
+    <div class="label">Operation Log</div>
+    <div class="process">
+      <!-- <div class="left-line"></div> -->
+      <div class="right-process-box">
+        <div class="process-item" v-for="(item, index) in processList" :key="index">
+          <div class="left-process-line">
+            <div class="icon-box"></div>
+            <div class="process-line" v-if="index !== processList.length - 1"></div>
+          </div>
+          <div class="process-content">
+            <p class="process-time">{{ item.time }}</p>
+            <div class="process-data">
+              <div class="process-data-label">{{ item.label }}</div>
+              <div class="process-data-user">{{ item.createdBy }}</div>
+              <div class="--process-data-tips">{{ item.tips }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.operation-log {
+  padding: 0 16px;
+  .label {
+    margin-bottom: 16px;
+    font-size: 18px;
+    font-weight: 700;
+  }
+  .process {
+    position: relative;
+    display: flex;
+    align-items: center;
+    .left-line {
+      position: absolute;
+      left: 8px;
+      top: 0;
+      bottom: 0;
+      width: 2px;
+      background-color: var(--color-neutral-3);
+    }
+    .right-process-box {
+      flex-grow: 1;
+      display: flex;
+      flex-direction: column;
+
+      .process-item {
+        display: flex;
+        align-items: center;
+        height: 126px;
+        .left-process-line {
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          height: 100%;
+          margin-right: 8px;
+          .icon-box {
+            width: 8px;
+            height: 8px;
+            border-radius: 50%;
+            background-color: var(--color-neutral-1);
+          }
+          .process-line {
+            flex: 1;
+            border-left: 1px dashed var(--color-neutral-1);
+          }
+        }
+
+        .process-content {
+          margin-top: -3px;
+          .process-time {
+            font-size: 14px;
+            font-weight: 500;
+            color: var(--color-neutral-1);
+          }
+          .process-data {
+            width: 480px;
+            padding: 8px;
+            margin-top: 8px;
+            margin-bottom: 16px;
+            border-radius: 12px;
+            background: var(--color-share-link-bg);
+            .process-data-label {
+              font-weight: 700;
+            }
+            .process-data-user {
+              margin-top: 4px;
+              font-size: 12px;
+              color: var(--color-neutral-2);
+            }
+            .--process-data-tips {
+              width: 464px;
+              height: 28px;
+              margin-top: 8px;
+              padding: 0 8px;
+              border-radius: 6px;
+              line-height: 28px;
+              background: var(--color---process-data-tips-bg);
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 95 - 0
src/views/DestinationDelivery/src/components/TableView/src/components/ShipmentInforTable.vue

@@ -0,0 +1,95 @@
+<script setup lang="ts">
+import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
+// import { autoWidth } from '@/utils/table'
+import { useRowClickStyle } from '@/hooks/rowClickStyle'
+import { formatTimezone, formatNumber } from '@/utils/tools'
+
+const props = defineProps({
+  data: Object
+})
+const tableRef = ref<VxeGridInstance | null>(null)
+const tableData = ref<VxeGridProps<any>>({
+  minHeight: 70,
+  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)'
+  },
+  columnConfig: { resizable: true, useKey: true },
+  rowConfig: { isHover: true }
+})
+
+const handleColumns = (columns: any) => {
+  const newColumns = columns.map((item: any) => {
+    let curColumn: any = {
+      title: item.title,
+      field: item.field,
+      minWidth: 30
+    }
+
+    // 格式化
+    if (item.formatter === 'date' || item.formatter === 'dateTime') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => formatTimezone(cellValue)
+      }
+    } else if (item.formatter === 'number') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => formatNumber(Number(cellValue), item?.digits)
+      }
+    }
+    return curColumn
+  })
+  return newColumns
+}
+watch(
+  () => props.data,
+  (newVal) => {
+    const containers = newVal?.containers
+    if (containers && containers.container_column) {
+      tableData.value.columns = handleColumns(containers.container_column)
+      tableData.value.data = containers.container_data
+      // nextTick(() => {
+      //   tableRef.value && autoWidth(tableData.value, tableRef.value)
+      // })
+    }
+  },
+  {
+    immediate: true,
+    deep: true
+  }
+)
+
+// 实现行点击样式
+useRowClickStyle(tableRef)
+</script>
+
+<template>
+  <div class="shipment-infor-table">
+    <div class="label">Shipment Information</div>
+    <vxe-grid ref="tableRef" v-bind="tableData">
+      <template #empty>
+        <div class="empty">No data</div>
+      </template>
+    </vxe-grid>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.shipment-infor-table {
+  margin-top: 20px;
+  padding: 8px 16px;
+  .label {
+    margin-bottom: 14px;
+    font-size: 18px;
+    font-weight: 700;
+  }
+}
+</style>

+ 0 - 1
src/views/Layout/src/components/Menu/MenuView.vue

@@ -204,7 +204,6 @@ const jumpLink = (link: string) => {
       @select="changeRouter"
       :default-active="activeMenu"
       :default-openeds="openeds"
-      :unique-opened="true"
       :collapse="isCollapse"
     >
       <template v-for="item in menuList" :key="item.index">