Sfoglia il codice sorgente

feat: 完善delivery

Jack Zhou 1 mese fa
parent
commit
a154553f1d

+ 2 - 0
src/styles/theme.scss

@@ -376,6 +376,7 @@
   --color-calendar-booking-tag-bg: rgba(130, 0, 218, 0.1);
    --color-calendar-booking-tag-text: #8200da;
   --color-calendar-booking-tag-label-text: rgba(130, 0, 218, 0.5);
+  --color-calendar-selected-cell-bg: #f4f4f5;
   
   --color-delivery-calendar-delivery-date-sign-bg:rgba(20, 71, 230, 0.1);
   --color-delivery-calendar-delivery-date-sign-border: rgba(20, 71, 230, 0.3);
@@ -640,6 +641,7 @@
   --color-calendar-booking-tag-bg: rgba(153, 1, 255, 0.2);
   --color-calendar-booking-tag-text: #fff;
   --color-calendar-booking-tag-label-text: rgba(255,255,255,0.5);
+  --color-calendar-selected-cell-bg: #3d4047;
   
   --color-delivery-calendar-delivery-date-sign-bg:rgba(31, 85, 255, 0.3);
   --color-delivery-calendar-delivery-date-sign-border: rgba(31, 85, 255, 0.7);

+ 14 - 4
src/views/DestinationDelivery/src/DestinationDelivery.vue

@@ -10,8 +10,11 @@ const listView = ref(null)
 const handleConfigurations = () => {
   router.push({ name: 'Configurations' })
 }
-const handleCreate = () => {
-  router.push({ name: 'Create New Booking' })
+const handleCreate = (date?: string) => {
+  router.push({
+    name: 'Create New Booking',
+    query: date ? { date } : undefined
+  })
 }
 
 const pageType = ref('Calendar View')
@@ -37,7 +40,7 @@ const directionOptions = [
         <el-button
           style="height: 38px"
           class="el-button--main el-button--pain-theme"
-          @click="handleCreate"
+          @click="handleCreate()"
           v-if="listView?.isEmployeeRole === false"
         >
           <span style="margin-right: 4px" class="font_family icon-icon_add_b"></span>
@@ -60,7 +63,11 @@ const directionOptions = [
     </div>
 
     <ListView ref="listView" v-if="pageType === 'List View'"></ListView>
-    <CalendarView v-if="pageType === 'Calendar View'"></CalendarView>
+    <CalendarView
+      @add="handleCreate"
+      :isEmployeeRole="listView?.isEmployeeRole"
+      v-if="pageType === 'Calendar View'"
+    ></CalendarView>
   </div>
 </template>
 
@@ -85,6 +92,9 @@ const directionOptions = [
   background-color: var(--color-mode);
 }
 .page-type {
+  position: relative;
+  z-index: 10;
+  width: 300px;
   margin: 10px 24px;
   .el-segmented {
     height: 40px;

+ 122 - 54
src/views/DestinationDelivery/src/components/CalendarTagDetailDialog.vue

@@ -2,7 +2,9 @@
 import { autoWidth } from '@/utils/table'
 import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
 import { useRowClickStyle } from '@/hooks/rowClickStyle'
+import { formatTimezone } from '@/utils/tools'
 import dayjs from 'dayjs'
+import { useThemeStore } from '@/stores/modules/theme'
 
 const dialogVisible = ref(false)
 
@@ -36,52 +38,51 @@ const tableRef = ref<VxeGridInstance | null>(null)
 // 实现行点击样式
 useRowClickStyle(tableRef)
 
-const columns = [
-  {
-    title: 'HBOL No.',
-    field: 'h_bol'
-  },
-  {
-    title: 'MBL No.',
-    field: 'm_bol'
-  },
-  {
-    title: 'Container No.',
-    field: 'ctnr'
-  },
-  {
-    title: 'Service Type',
-    field: 'service_type'
-  },
-  {
-    title: 'PO No.',
-    field: 'po_no'
-  },
-  {
-    title: 'Reference No.',
-    field: 'referebce_no'
-  },
-  {
-    title: 'Mode',
-    field: 'transport_mode'
-  },
-  {
-    title: 'Packages',
-    field: 'packages'
-  }
-]
+const getTableColumns = () => {
+  $api
+    .BookingTableColumn({
+      reset: 'yes'
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        tableData.value.columns = [...handleColumns(res.data.TrackingTableColumns)]
+      }
+    })
+}
+onMounted(() => {
+  getTableColumns()
+})
 const handleColumns = (columns: any) => {
   const newColumns = columns.map((item: any) => {
     let curColumn: any = {
       title: item.title,
       field: item.field,
-      minWidth: 30
+      width: '150px'
     }
-
-    if (item.field === 'file') {
+    // 格式化
+    if (item.formatter === 'date') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => formatTimezone(cellValue)
+      }
+    } else if (item.type === 'link') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'trackingNo' }
+      }
+    } else if (item.type == 'recommend') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => {
+          if (!cellValue) return ''
+          const array = cellValue?.split('-')
+          return `${formatTimezone(array[0])} - ${formatTimezone(array[1])}`
+        }
+      }
+    } else if (item.type === 'download') {
       curColumn = {
         ...curColumn,
-        slots: { default: 'file' }
+        slots: { default: 'download' }
       }
     }
     return curColumn
@@ -89,23 +90,75 @@ const handleColumns = (columns: any) => {
   return newColumns
 }
 
-tableData.value.columns = handleColumns(columns)
 const title = ref('')
 const pageData = ref()
 const tagType = ref()
 const openDialog = (type, date, data) => {
-  console.log(type, date, 'openDialog', data)
   dialogVisible.value = true
   pageData.value = data
   tagType.value = type
-  if (type === 'ending') {
-    tableData.value.data = data['endingDetail']
-  } else if (type === 'delivery') {
-    title.value = `Recommended Delivery Shipments for ${dayjs(date).format('YYYY-MM-DD')}`
-    tableData.value.data = data['shipmentDetail']
+
+  title.value = `Recommended Delivery Shipments for ${dayjs(date).format('YYYY-MM-DD')}`
+  tableData.value.data = data['shipmentDetail']
+  nextTick(() => {
+    tableRef.value &&
+      autoWidth(tableData.value, tableRef.value, {
+        packing_list: 190,
+        commercial_invoice: 180
+      })
+  })
+}
+const themeStore = useThemeStore()
+const rowStyle = ({ row }) => {
+  if (row.is_ending) {
+    return {
+      backgroundColor:
+        themeStore.theme === 'dark' ? 'rgba(255, 18, 28, 0.2)' : 'rgba(193, 0, 8, 0.1)'
+    }
   }
 }
+function base64ToBlob(base64, mimeType) {
+  const byteString = atob(base64.split(',')[0].startsWith('data:') ? base64.split(',')[1] : base64)
+  const ab = new ArrayBuffer(byteString.length)
+  const ia = new Uint8Array(ab)
+  for (let i = 0; i < byteString.length; i++) {
+    ia[i] = byteString.charCodeAt(i)
+  }
+  return new Blob([ia], { type: mimeType })
+}
+const handleDownload = (serialNo: string, field: string) => {
+  const fileType = field === 'commercial_invoice' ? 'C/I' : 'Packing List'
+  $api
+    .downloadBookingTableFile({
+      serial_no: serialNo,
+      file_type: fileType
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        // 使用
+        const base64 = res.data.data // 纯 base64 字符串,不含 data: 前缀
+        const mimeType = 'application/octet-stream'
+        const fileName = res.data.filename || 'download'
+
+        const blob = base64ToBlob(base64, mimeType)
+        const url = URL.createObjectURL(blob)
 
+        const link = document.createElement('a')
+        link.href = url
+        link.download = fileName
+        document.body.appendChild(link)
+        link.click()
+        document.body.removeChild(link)
+
+        // 可选:下载完成后释放内存
+        link.onclick = () => {
+          setTimeout(() => {
+            URL.revokeObjectURL(url) // 释放对象 URL
+          }, 100)
+        }
+      }
+    })
+}
 defineExpose({
   openDialog
 })
@@ -128,15 +181,23 @@ defineExpose({
       </template>
       <span style="display: inline-block; color: var(--color-neutral-2)"
         >Total:
-        {{ tagType === 'delivery' ? pageData.shipmentNumber : pageData.endingNumber }}
+        {{ pageData.shipmentNumber }}
         shipments</span
       >
       <span style="color: var(--color-neutral-2)"> | </span>
       <span style="color: var(--color-neutral-2)"
-        >Total Cartons:
-        {{ tagType === 'delivery' ? pageData.shipmentCtns : pageData.endingCtns }} ctns</span
+        >Total Cartons: {{ pageData.shipmentCtns }} ctns</span
       >
-      <vxe-grid style="margin-top: 8px" ref="tableRef" v-bind="tableData">
+      <vxe-grid style="margin-top: 8px" ref="tableRef" v-bind="tableData" :row-style="rowStyle">
+        <template #download="{ row, column }">
+          <div class="download-btn" @click="handleDownload(row.h_serial_no, column.field)">
+            <span class="font_family icon-icon_download_b icon-style"> </span>
+            <span
+              >{{ row.h_bol
+              }}{{ column.field === 'commercial_invoice' ? '.CI.zip' : '._PL.zip' }}</span
+            >
+          </div>
+        </template>
         <template #empty>
           <div class="empty">No data</div>
         </template>
@@ -162,13 +223,9 @@ defineExpose({
   border: 0.5px dashed var(--color-calendar-ending-tag-border);
   background-color: var(--color-calendar-ending-tag-bg);
   .font_family,
-  .type,
-  .ctns-tag {
+  .type {
     color: var(--color-calendar-ending-tag-text) !important;
   }
-  .ctns-tag {
-    background-color: var(--color-calendar-ending-tag-bg);
-  }
 }
 .tag-style {
   display: flex;
@@ -188,4 +245,15 @@ defineExpose({
     line-height: 16px;
   }
 }
+.download-btn {
+  cursor: pointer;
+
+  &:hover,
+  &:focus {
+    span,
+    .icon-style {
+      color: var(--color-theme) !important;
+    }
+  }
+}
 </style>

+ 97 - 25
src/views/DestinationDelivery/src/components/CalendarView.vue

@@ -1,8 +1,11 @@
 <script setup lang="ts">
-import { ref, computed } from 'vue'
+import { ref, computed, onMounted } from 'vue'
 import type { Dayjs } from 'dayjs'
 import dayjs from 'dayjs'
 import CalendarTagDetailDialog from './CalendarTagDetailDialog.vue'
+import { useUserStore } from '@/stores/modules/user'
+
+const userStore = useUserStore()
 
 // 控制日历显示的月份(受控状态)
 const displayMonth = ref<Dayjs>(dayjs())
@@ -10,12 +13,12 @@ const displayMonth = ref<Dayjs>(dayjs())
 // 年份选项
 const yearOptions = computed(() => {
   const currentYear = new Date().getFullYear()
-  const startYear = 1100 // 你可以改成任意起始年,比如 1990
+  const startYear = 1950 // 你可以改成任意起始年,比如 1990
   const years = []
   for (let y = currentYear; y >= startYear; y--) {
     years.push(y)
   }
-  return years // [2026, 2025, ..., 2000]
+  return years //
 })
 
 const calendarData = ref({})
@@ -23,12 +26,25 @@ const calendarData = ref({})
 const getDataByDate = (date: Dayjs, key: string) => {
   return calendarData.value[date.format('YYYY-MM-DD')]?.[key] ?? 0
 }
+
+const emit = defineEmits(['add'])
+const calendarLoading = ref(false)
+const handleAddClick = (date) => {
+  emit('add', dayjs(date).format('YYYY-MM-DD'))
+}
+
 const getPageData = () => {
-  $api.getDeliveryCalendarData({ month: dayjs().format('MM/YYYY') }).then((res) => {
-    if (res.code === 200) {
-      calendarData.value = res.data
-    }
-  })
+  calendarLoading.value = true
+  $api
+    .getDeliveryCalendarData({ month: dayjs().format('MM/YYYY') })
+    .then((res) => {
+      if (res.code === 200) {
+        calendarData.value = res.data
+      }
+    })
+    .finally(() => {
+      calendarLoading.value = false
+    })
 }
 
 onMounted(() => {
@@ -41,13 +57,6 @@ const onPanelChange = (value: Dayjs) => {
   displayMonth.value = value
 }
 
-// 可选:处理日期点击(比如查看详情)
-const onSelect = (date: Dayjs) => {
-  console.log('选中日期:', date.format('YYYY-MM-DD'))
-  // 你可以在这里打开详情弹窗等
-}
-
-// 年份切换
 const handleYearChange = (year: number, onChange: (date: Dayjs) => void) => {
   const newDate = displayMonth.value.clone().year(year)
   onChange(newDate)
@@ -62,6 +71,7 @@ const handleMonthChange = (month: number, onChange: (date: Dayjs) => void) => {
 }
 
 const calendarTagDialog = ref()
+
 const handleTagClick = (type, date) => {
   calendarTagDialog.value.openDialog(type, date, calendarData.value[date.format('YYYY-MM-DD')])
 }
@@ -70,7 +80,12 @@ const handleTagClick = (type, date) => {
 <template>
   <div class="calendar-container">
     <!-- 👇 绑定 :value + 监听 @panelChange -->
-    <a-calendar :value="displayMonth" fullscreen @select="onSelect" @panelChange="onPanelChange">
+    <a-calendar
+      :value="displayMonth"
+      v-vloading="calendarLoading"
+      fullscreen
+      @panelChange="onPanelChange"
+    >
       <!-- 自定义头部:value 来自 displayMonth(已同步) -->
       <template #headerRender="{ value, onChange }">
         <div class="custom-header">
@@ -93,7 +108,7 @@ const handleTagClick = (type, date) => {
             @change="(year) => handleYearChange(year, onChange)"
           >
             <a-select-option v-for="y in yearOptions" :key="y" :value="y">
-              {{ y }}
+              {{ y }}
             </a-select-option>
           </a-select>
 
@@ -134,7 +149,11 @@ const handleTagClick = (type, date) => {
               dayjs(current).isSame(dayjs(displayMonth), 'month')
             "
           >
-            <div class="ending-tag tag-style" @click="handleTagClick('ending', current)">
+            <div
+              v-if="getDataByDate(current, 'endingNumber') || getDataByDate(current, 'endingCtns')"
+              class="ending-tag tag-style"
+              @click="handleTagClick('ending', current)"
+            >
               <span
                 class="font_family icon-icon_delay_b1"
                 style="margin-right: 3px; font-size: 13px"
@@ -142,7 +161,13 @@ const handleTagClick = (type, date) => {
               <span class="type">{{ getDataByDate(current, 'endingNumber') }} Ending</span>
               <span class="ctns-tag">{{ getDataByDate(current, 'endingCtns') }} ctns</span>
             </div>
-            <div class="delivery-tag" @click="handleTagClick('delivery', current)">
+            <div
+              v-if="
+                getDataByDate(current, 'shipmentNumber') || getDataByDate(current, 'shipmentCtns')
+              "
+              class="delivery-tag"
+              @click="handleTagClick('delivery', current)"
+            >
               <div class="label">Recommended Delivery</div>
               <div class="tag-style">
                 <span class="font_family icon-icon_road__booking_b" style="font-size: 12px"></span>
@@ -150,7 +175,12 @@ const handleTagClick = (type, date) => {
                 <span class="ctns-tag">{{ getDataByDate(current, 'shipmentCtns') }} ctns</span>
               </div>
             </div>
-            <div class="booking-tag">
+            <div
+              class="booking-tag"
+              v-if="
+                getDataByDate(current, 'bookingNumber') || getDataByDate(current, 'bookingCtns')
+              "
+            >
               <div class="label">Destination Booking</div>
               <div class="tag-style">
                 <span class="font_family icon-icon_booking_order_b" style="font-size: 12px"></span>
@@ -159,6 +189,14 @@ const handleTagClick = (type, date) => {
               </div>
             </div>
           </div>
+          <!-- 新增图标 -->
+          <div
+            class="add-icon"
+            @click.stop="handleAddClick(current)"
+            v-if="userStore.userInfo.user_type.toLowerCase() !== 'employee'"
+          >
+            <span class="font_family icon-icon_add_b"></span>
+          </div>
         </ul>
       </template>
     </a-calendar>
@@ -167,8 +205,9 @@ const handleTagClick = (type, date) => {
 </template>
 
 <style scoped lang="scss">
-/* ... 你的深色样式保持不变 ... */
 .calendar-container {
+  // position: relative;
+  // z-index: 99;
   margin-top: -46px;
   padding: 0 24px;
 }
@@ -211,8 +250,8 @@ const handleTagClick = (type, date) => {
   .free-storage-period-ends {
     margin-right: 8px;
     .sign {
-      background-color: var(--color-delivery-calendar-ends-sign-bg);
-      border: 1px dashed var(--color-delivery-calendar-ends-sign-border);
+      background-color: var(--color-calendar-ending-tag-bg);
+      border: 1px dashed var(--color-calendar-ending-tag-border);
     }
     .label {
       color: var(--color-delivery-calendar-ends-label-text);
@@ -295,10 +334,14 @@ const handleTagClick = (type, date) => {
 :deep(.ant-picker-calendar-date-selected),
 :deep(.ant-picker-cell-selected .ant-picker-calendar-date) {
   background: transparent !important;
-  color: #e0e0e0 !important;
+  color: var(--color-neutral-1) !important;
   box-shadow: none !important;
 }
-
+:deep(td.ant-picker-cell-selected) {
+  &:hover {
+    background: var(--color-calendar-selected-cell-bg) !important;
+  }
+}
 // 不是当月日期时的样式
 :deep(
   td.ant-picker-cell:not(.ant-picker-cell-in-view):not(.custom-delivery-calendar .ant-picker-cell)
@@ -329,14 +372,43 @@ const handleTagClick = (type, date) => {
 }
 
 .events {
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-end;
   list-style: none;
   margin: 0;
   padding: 0;
+  padding-bottom: 6px;
   height: 110px;
   overflow-y: auto;
   font-size: 12px;
 }
 
+.add-icon {
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  background-color: var(--color-theme);
+  border-radius: 50%;
+  width: 16px;
+  height: 16px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  opacity: 0;
+  transition: opacity 0.3s ease;
+
+  .font_family {
+    display: inline-block;
+    font-size: 12px;
+    color: #fff !important;
+  }
+}
+
+.ant-picker-cell-in-view:hover .add-icon {
+  opacity: 1;
+}
+
 .events .tags-details {
   display: flex;
   align-items: center;

+ 10 - 7
src/views/DestinationDelivery/src/components/CreateNewBooking/src/CreateNewbooking.vue

@@ -7,20 +7,23 @@ import NotAvailable from './images/default_destination_not_available@2x.png'
 import NotShipment from './images/default_no_shipment@2x.png'
 import submitsucessful from './images/icon_success_big@2x.png'
 import { useUserStore } from '@/stores/modules/user'
-import { useRouter } from 'vue-router'
+import { useRouter, useRoute } from 'vue-router'
 import { ElMessage } from 'element-plus'
+import dayjs from 'dayjs'
 
 const userStore = useUserStore()
 const router = useRouter()
-const { currentRoute } = router
-const { value } = currentRoute
-const { query } = value
-const { a } = query
+const route = useRoute()
+const a = route.query.a
 
 const CreateNewBOokingSearch = ref('')
 const status = ref('')
 const booking = ref('')
-const DateValue = ref('')
+const DateValue = ref(
+  route.query.date && dayjs(route.query.date as string).isValid()
+    ? dayjs(route.query.date as string).format('YYYY.MM.DD')
+    : ''
+)
 const DeliveryTime = ref('')
 const bookingTableRef = ref()
 const VesselName = ref([])
@@ -1473,7 +1476,7 @@ onMounted(() => {
   position: absolute;
   top: -7px;
   left: 10px;
-  background: white; /* 用背景色覆盖边框 */
+  background: var(--color-mode); /* 用背景色覆盖边框 */
   padding: 0 5px;
   font-size: 12px;
   color: var(--color-neutral-2);

+ 1 - 1
src/views/DestinationDelivery/src/components/CreateNewBooking/src/components/NewbookingTable.vue

@@ -335,7 +335,7 @@ defineExpose({
     >
       <!-- download下载的插槽 -->
       <template #download="{ row, column }">
-        <div class="download-btn" @click="handleDownload(row.serial_no, column.field)">
+        <div class="download-btn" @click="handleDownload(row.h_serial_no, column.field)">
           <span class="font_family icon-icon_download_b icon-style"> </span>
           <span
             >{{ row.h_bol

+ 1 - 1
src/views/DestinationDelivery/src/components/ListView.vue

@@ -4,7 +4,7 @@ import TableView from './TableView'
 import DeliveryDate from './DeliveryDate.vue'
 
 const filterRef: Ref<HTMLElement | null> = ref(null)
-const containerHeight = useCalculatingHeight(document.documentElement, 376, [filterRef])
+const containerHeight = useCalculatingHeight(document.documentElement, 456, [filterRef])
 
 const queryData = ref({
   text_search: '',