Jelajahi Sumber

feat: ✨ 完成库存明细

@sun-chaoqun 2 tahun lalu
induk
melakukan
81fd8398ea

+ 1 - 0
components.d.ts

@@ -45,6 +45,7 @@ declare module '@vue/runtime-core' {
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElPagination: typeof import('element-plus/es')['ElPagination']
+    ElPopover: typeof import('element-plus/es')['ElPopover']
     ElRadio: typeof import('element-plus/es')['ElRadio']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']

+ 11 - 2
src/components/dialog/Dialog.vue

@@ -3,9 +3,11 @@ import { ref } from 'vue'
 const dialogVisible = ref(false)
 interface DialogProps {
   width?: string
+  alignCenter: boolean
 }
 withDefaults(defineProps<DialogProps>(), {
-  width: '50%'
+  width: '50%',
+  alignCenter: false
 })
 
 defineEmits<{ (e: 'on-close', value: boolean): void }>()
@@ -24,7 +26,14 @@ defineExpose({
 </script>
 
 <template>
-  <el-dialog v-model="dialogVisible" @close="DialogClose" @closed="DialogClose" :width="width" class="bottomD">
+  <el-dialog
+    v-model="dialogVisible"
+    @close="DialogClose"
+    @closed="DialogClose"
+    :align-center="alignCenter"
+    :width="width"
+    class="bottomD"
+  >
     <!-- 默认插槽 -->
     <slot></slot>
     <!-- 弹框头部 -->

+ 3 - 3
src/hooks/useTable.ts

@@ -59,10 +59,10 @@ export const useTable = (
       page_z: state.pageable.pageSize
     }
     const res = await requestApi(params)
-    state.tableData = res.Data.Data
+    state.tableData = res.Data?.Data
     dataCallback && (state.tableData = dataCallback(res))
-    state.pageable.total = res.Data.Num
-    if (res.Data.RemainingTime) {
+    state.pageable.total = res.Data?.Num
+    if (res.Data?.RemainingTime) {
       state.pageable.RemainingTime = res.Data.RemainingTime
     }
   }

+ 1 - 1
src/utils/common.ts

@@ -20,7 +20,7 @@ export const isEmptyObject = (obj: object) => Object.keys(obj).length > 0
  * @param date new Date
  * @returns string
  */
-export const dayJs = (date: Date | string) => dayjs(date)
+export const dayJs = (date?: Date | string) => dayjs(date)
 
 /**
  *

+ 0 - 152
src/utils/loginBg.ts

@@ -1,152 +0,0 @@
-// class StarrySky {
-//   canvas: HTMLCanvasElement
-//   ctx: CanvasRenderingContext2D
-//   particles: Particle[]
-//   count: number
-//   // actions: string[]
-//   // action: number
-//   constructor() {
-//     this.canvas = document.createElement('canvas') as HTMLCanvasElement
-//     this.canvas.width = innerWidth
-//     this.canvas.height = innerHeight
-//     this.canvas.style.zIndex = '-1'
-//     this.canvas.style.position = 'fixed'
-//     this.canvas.style.top = '0'
-//     this.canvas.style.left = '0'
-//     this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D
-//     document.body.appendChild(this.canvas)
-
-//     this.particles = []
-//     this.count = 1000
-
-//     // this.animate()
-//   }
-//   draw() {
-//     this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
-//     if (this.particles.length < this.count) {
-//       this.particles.push(new Particle(this.canvas.width, this.canvas.height, this.ctx))
-//     }
-//     for (let i in this.particles) {
-//       const p = this.particles[i]
-//       p.update()
-//       p.draw()
-//     }
-//   }
-// }
-
-// class Particle {
-//   x: number
-//   y: number
-//   vx: number
-//   w: number
-//   h: number
-//   ctx: CanvasRenderingContext2D
-//   constructor(width: number, height: number, ctx: CanvasRenderingContext2D) {
-//     this.w = width
-//     this.h = height
-//     this.ctx = ctx
-//     this.x = Math.random() * width
-//     this.y = Math.random() * height
-//     this.vx = Math.random()
-//   }
-//   update() {
-//     this.x += this.vx * 3
-//     if (this.x > this.w) {
-//       this.x = 0
-//     }
-//   }
-//   draw() {
-//     this.ctx.beginPath()
-//     this.ctx.arc(this.x, this.y, 1 + this.vx, 0, Math.PI * 2)
-//     this.ctx.fillStyle = `rgba(255, 255, 255, ${this.vx})`
-//     this.ctx.fill()
-//   }
-// }
-
-// export default StarrySky
-
-class StarrySky {
-  canvas: HTMLCanvasElement
-  ctx: CanvasRenderingContext2D
-  particles: Particle[]
-  count: number
-  constructor() {
-    this.canvas = document.createElement('canvas')
-    this.canvas.width = innerWidth
-    this.canvas.height = innerHeight
-    this.canvas.style.zIndex = '-1'
-    this.canvas.style.position = 'fixed'
-    this.canvas.style.top = '0'
-    this.canvas.style.left = '0'
-    this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D
-    document.body.appendChild(this.canvas)
-
-    this.particles = []
-    this.count = 1000
-
-    this.animate()
-    this.draw()
-  }
-  draw() {
-    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
-    for (let i = 0; i < this.count; i++) {
-      this.particles.push(new Particle(this.canvas.width, this.canvas.height, this.ctx))
-      const p = this.particles[i]
-      p.draw()
-    }
-  }
-  onresize() {
-    window.onresize = () => {
-      this.canvas.width = window.innerWidth
-      this.canvas.height = window.innerHeight
-      this.draw()
-    }
-  }
-  shanshuo() {
-    let index = Math.ceil(Math.random() * this.count)
-    this.particles.splice(0, index + 5)
-    if (this.particles.length < this.count) {
-      let particle = new Particle(this.canvas.width, this.canvas.height, this.ctx)
-      this.particles.push(particle)
-      particle.draw()
-    }
-  }
-  animate() {
-    requestAnimationFrame(() => this.animate())
-    this.shanshuo()
-  }
-}
-
-class Particle {
-  x: number
-  y: number
-  vx: number
-  w: number
-  h: number
-  ctx: CanvasRenderingContext2D
-  constructor(width: number, height: number, ctx: CanvasRenderingContext2D) {
-    this.w = width
-    this.h = height
-    this.ctx = ctx
-    this.x = Math.random() * width
-    this.y = Math.random() * height
-    this.vx = Math.random()
-  }
-  update() {
-    this.x += this.vx * 3
-    if (this.x > this.w) {
-      this.x = 0
-    }
-  }
-  draw() {
-    this.ctx.beginPath()
-    this.ctx.arc(this.x, this.y, 1 + this.vx, 0, Math.PI * 2)
-    this.ctx.fillStyle = `rgba(255, 255, 255, ${this.vx})`
-    this.ctx.fill()
-  }
-}
-
-// const star = new StarrySky()
-// star.onresize()
-
-export default StarrySky

+ 239 - 19
src/views/storehouse/InventoryStatistics.vue

@@ -2,17 +2,26 @@
 import {
   Storehouse_Stock_List,
   Storehouse_Depot_List,
+  Storehouse_ProductClass_List,
+  Storehouse_Product_Model_List,
+  Storehouse_Product_Name_List,
   Storehouse_Stock_Detail_List,
   Storehouse_Stock_Detail_Excel
 } from '@/api/storehouse/index'
-import { ref, reactive, nextTick, onMounted } from 'vue'
+import { ref, reactive, onMounted } from 'vue'
 import { List, Picture, ArrowUpBold, ArrowDownBold } from '@element-plus/icons-vue'
+import Drawer from '@/components/Drawer/index.vue'
 import { GlobalStore } from '@/stores/index'
 import TableBase from '@/components/TableBase/index.vue'
 import type { ColumnProps } from '@/components/TableBase/interface/index'
+import { dayJs } from '@/utils/common'
+
 const globalStore = GlobalStore()
+const DrawerRef = ref<InstanceType<typeof Drawer> | null>(null)
 const TableRef = ref<InstanceType<typeof TableBase> | null>(null)
+const TableDetailRef = ref<InstanceType<typeof TableBase> | null>(null)
 
+let timeout: NodeJS.Timeout
 const searchShow = ref(false)
 const initParam = reactive({
   User_tokey: globalStore.GET_User_tokey,
@@ -21,9 +30,15 @@ const initParam = reactive({
   T_product_name: '',
   T_product_model: ''
 })
+const detailInitParam = reactive({
+  T_depot_id: '',
+  T_product_id: '',
+  T_start_date: '',
+  T_end_date: ''
+})
 const columns: ColumnProps[] = [
   { type: 'index', label: '序号', width: 80 },
-  { prop: 'T_iccid', label: '仓库名称', ellipsis: true },
+  { prop: 'T_depot_name', label: '仓库名称' },
   { prop: 'T_product_img', label: '产品图片', name: 'T_product_img' },
   { prop: 'T_product_name', label: '产品名称' },
   { prop: 'T_product_class_name', label: '产品分类' },
@@ -33,8 +48,50 @@ const columns: ColumnProps[] = [
   { prop: 'operation', label: '操作', width: 200, fixed: 'right' }
 ]
 
-const searchShowHandle = () => {
-  searchShow.value = !searchShow.value
+const detailColumns: ColumnProps[] = [
+  { type: 'index', label: '序号', width: 80 },
+  { prop: 'T_product_name', label: '产品名称' },
+  { prop: 'T_product_model', label: '产品型号', ellipsis: true },
+  { prop: 'T_product_spec', label: '产品规格' },
+  { prop: 'T_month', label: '月份' },
+  { prop: 'T_beginning', label: '期初库存' },
+  { prop: 'T_in', label: '入库' },
+  { prop: 'T_out', label: '出库' },
+  { prop: 'T_ending', label: '期末库存' }
+]
+
+const searchShowHandle = () => (searchShow.value = !searchShow.value)
+const searchHandle = () => {
+  TableRef.value?.searchTable()
+}
+const dataCallback = (res: any) => {
+  return res.Data.Data.map((item: any) => {
+    depotOptions.value.forEach((depot: any) => {
+      if (item.T_depot_id === depot.Id) {
+        item.T_depot_name = depot.T_name
+      }
+    })
+    return item
+  })
+}
+const detailDataCallback = (res: any) => res.Data
+/**
+ * 导出表格
+ */
+const T_date = ref<string[]>([]) // 作为初始值
+const T_date_detail = ref<string[]>([])
+const T_date_export = ref<string[]>([])
+const visible = ref(false)
+const exportExcel = () => (visible.value = true)
+const confirmExpor = async () => {
+  const params = {
+    User_tokey: globalStore.GET_User_tokey,
+    T_depot_id: initParam.T_depot_id,
+    T_start_date: T_date.value[0],
+    T_end_date: T_date.value[1]
+  }
+  const res = await Storehouse_Stock_Detail_Excel(params)
+  console.log(res)
 }
 
 // 拿到仓库列表
@@ -42,43 +99,166 @@ interface ItemType {
   T_name: string
   Id: number
 }
-const options = ref<ItemType[]>([])
+const depotOptions = ref<ItemType[]>([])
+const classOptions = ref<any[]>([])
+const modelOptions = ref<any[]>([])
+// 获取产品分类
+const getProductClassList = async () => {
+  const res: any = await Storehouse_ProductClass_List({ page: 1, page_z: 999 })
+  classOptions.value = res.Data.Data
+}
+/**
+ * 获取仓库列表
+ */
 const getDepotList = async () => {
-  if (globalStore.GET_depotList.length) return
+  if (globalStore.GET_depotList.length) {
+    depotOptions.value = globalStore.GET_depotList
+    return
+  }
   const res: any = await Storehouse_Depot_List({ User_tokey: globalStore.GET_User_tokey, page: 1, page_z: 999 })
-  options.value = res.Data.Data
-  globalStore.SET_depotList(options.value)
+  depotOptions.value = res.Data.Data
+  globalStore.SET_depotList(depotOptions.value)
+}
+/**
+ * 模糊搜索名称
+ * @param str 名称字符串
+ */
+const getNameAsync = async (str: string): Promise<any> => {
+  const res: any = await Storehouse_Product_Name_List({ T_name: str, T_class: initParam.T_product_class })
+  if (!res.Data) return
+  return res.Data.map((item: any, index: number) => {
+    return {
+      value: item,
+      index: index
+    }
+  })
+}
+/**
+ * 模糊搜索名称
+ * @param queryString 输入框的输入字符
+ * @param cb 异步搜索后的回调
+ */
+const querySearchAsync = async (queryString: string, cb: (arg: any) => void) => {
+  clearTimeout(timeout)
+  globalStore.SET_isloading(true)
+  timeout = setTimeout(async () => {
+    const results = await getNameAsync(queryString)
+    cb(results)
+    globalStore.SET_isloading(false)
+  }, 2000)
+}
+/**
+ * 异步获取产品型号
+ */
+const getProductModelList = async () => {
+  globalStore.SET_isloading(true)
+  const res: any = await Storehouse_Product_Model_List({ T_name: initParam.T_product_name })
+  modelOptions.value = res.Data.map((item: any, index: number) => {
+    return {
+      value: item,
+      index: index
+    }
+  })
+  globalStore.SET_isloading(false)
+}
+const handleSelect = (item: any) => {
+  initParam.T_product_name = item.value
+  getProductModelList()
+}
+
+// 明细
+const callbackDrawer = (done: () => void) => done()
+const previewDetail = (id: string) => {
+  DrawerRef.value?.openDrawer()
+  T_date_detail.value = [...T_date.value]
+  if (detailInitParam.T_product_id === id) return
+  detailInitParam.T_depot_id = initParam.T_depot_id
+  detailInitParam.T_product_id = id
+  detailInitParam.T_start_date = T_date.value[0]
+  detailInitParam.T_end_date = T_date.value[1]
+  TableDetailRef.value?.searchTable()
+}
+const dateDetailChange = (str: string[]) => {
+  if (!str) return
+  detailInitParam.T_start_date = str[0]
+  detailInitParam.T_end_date = str[1]
+  TableDetailRef.value?.searchTable()
 }
 onMounted(() => {
   getDepotList()
+  getProductClassList()
+
+  T_date.value = [dayJs().startOf('year').format('YYYY-MM-DD'), dayJs().format('YYYY-MM-DD')]
+  T_date_export.value = [...T_date.value]
 })
 </script>
 
 <template>
   <div class="inventory-statistics">
-    <TableBase ref="TableRef" :columns="columns" :requestApi="Storehouse_Stock_List" :initParam="initParam">
+    <TableBase
+      ref="TableRef"
+      :columns="columns"
+      :requestApi="Storehouse_Stock_List"
+      :initParam="initParam"
+      :dataCallback="dataCallback"
+    >
       <template #table-header>
         <div class="head-search" :class="searchShow ? 'active' : ''">
           <el-form :model="initParam" class="result-form" label-width="100px">
             <el-row>
               <el-col :span="6"
-                ><el-form-item label="产品分类" :inline-message="true"> <el-input /> </el-form-item
+                ><el-form-item label="产品分类" :inline-message="true">
+                  <el-select v-model="initParam.T_product_class" class="w-50 m-2" clearable placeholder="请选择分类~">
+                    <el-option v-for="item in classOptions" :key="item.Id" :label="item.T_name" :value="item.Id" />
+                  </el-select> </el-form-item
               ></el-col>
               <el-col :span="6"
-                ><el-form-item label="产品名称"> <el-input /> </el-form-item
+                ><el-form-item label="产品名称">
+                  <el-autocomplete
+                    v-model="initParam.T_product_name"
+                    clearable
+                    :fetch-suggestions="querySearchAsync"
+                    placeholder="按产品名称搜索"
+                    :debounce="2000"
+                    :trigger-on-focus="false"
+                    @select="handleSelect"
+                  /> </el-form-item
               ></el-col>
               <el-col :span="6"
-                ><el-form-item label="产品型号"> <el-input /> </el-form-item
+                ><el-form-item label="产品型号">
+                  <el-select v-model="initParam.T_product_model" clearable placeholder="请选择型号~">
+                    <el-option v-for="item in modelOptions" :key="item.index" :label="item.value" :value="item.value" />
+                  </el-select> </el-form-item
               ></el-col>
               <el-col :span="6">
-                <el-button :icon="ArrowDownBold" @click="searchShowHandle" />
-                <el-button type="primary">搜索</el-button>
-                <el-button type="success">导出</el-button>
+                <el-button :icon="searchShow ? ArrowUpBold : ArrowDownBold" @click="searchShowHandle" />
+                <el-button type="primary" @click="searchHandle">搜索</el-button>
+                <el-popover :visible="visible" placement="bottom" :width="400" trigger="click">
+                  <template #reference>
+                    <el-button type="success" @click="exportExcel">导出</el-button>
+                  </template>
+                  <el-date-picker
+                    v-model="T_date_export"
+                    type="daterange"
+                    range-separator="~"
+                    start-placeholder="开始时间"
+                    end-placeholder="结束时间"
+                    format="YYYY-MM-DD"
+                    value-format="YYYY-MM-DD"
+                  />
+                  <div class="export-popover">
+                    <el-button size="small" @click="visible = false">取消</el-button>
+                    <el-button size="small" type="primary" @click="confirmExpor">确认</el-button>
+                  </div>
+                </el-popover>
               </el-col>
             </el-row>
             <el-row class="search-bottom">
               <el-col :span="6"
-                ><el-form-item label="仓库列表" :inline-message="true"> <el-input /> </el-form-item
+                ><el-form-item label="仓库列表" :inline-message="true">
+                  <el-select v-model="initParam.T_depot_id" class="w-50 m-2" clearable placeholder="请选择分类~">
+                    <el-option v-for="item in depotOptions" :key="item.Id" :label="item.T_name" :value="item.Id" />
+                  </el-select> </el-form-item
               ></el-col>
             </el-row>
           </el-form>
@@ -100,19 +280,59 @@ onMounted(() => {
         </el-image>
       </template>
       <template #right="{ row }">
-        <el-button link type="primary" size="small" :icon="List">明细</el-button>
+        <el-button link type="primary" size="small" :icon="List" @click="previewDetail(row.T_product_id)"
+          >明细</el-button
+        >
       </template>
     </TableBase>
+    <Drawer ref="DrawerRef" :handleClose="callbackDrawer" size="80%">
+      <template #header="{ params }">
+        <h4 :id="params.titleId" :class="params.titleClass">库存明细</h4>
+      </template>
+      <TableBase
+        ref="TableDetailRef"
+        :pagination="false"
+        :columns="detailColumns"
+        :requestApi="Storehouse_Stock_Detail_List"
+        :initParam="detailInitParam"
+        :dataCallback="detailDataCallback"
+      >
+        <template #table-header>
+          <el-row class="head-search">
+            <el-col :span="12"
+              ><el-form-item label="产品名称">
+                <el-date-picker
+                  v-model="T_date_detail"
+                  type="daterange"
+                  range-separator="~"
+                  start-placeholder="开始时间"
+                  end-placeholder="结束时间"
+                  format="YYYY-MM-DD"
+                  value-format="YYYY-MM-DD"
+                  @change="dateDetailChange" /></el-form-item
+            ></el-col>
+          </el-row>
+        </template>
+      </TableBase>
+    </Drawer>
   </div>
 </template>
 
 <style scoped lang="scss">
 @import '@/styles/var.scss';
+.export-popover {
+  padding: 5px;
+  display: flex;
+  justify-content: center;
+}
+:deep(.drawer__content) {
+  @include f-direction;
+}
 .inventory-statistics {
   @include f-direction;
   .head-search {
     width: 100%;
-    height: 32px;
+    height: 33px;
     overflow: hidden;
     transition: all 0.5s ease-in-out;
     .result-form.el-form .el-form-item {
@@ -123,7 +343,7 @@ onMounted(() => {
     }
   }
   .head-search.active {
-    height: 82px;
+    height: 85px;
   }
   .image-slot {
     display: flex;