YangJian0701 8 months ago
parent
commit
841f59bb4f

BIN
ERP.rar


+ 9 - 0
src/api/storehouse/index.ts

@@ -112,6 +112,11 @@ export const StockIn_ListProducts = (params: any) => $http.post('/storage/StockI
 export const Storehouse_StockIn_Get = (params: any) => $http.post('/storage/StockIn/Get', params)
 // 入库
 export const Storehouse_StockIn_Add = (params: any) => $http.post('/storage/StockIn/Add', params)
+// 编辑
+export const Storehouse_StockIn_Edit = (params: any) => $http.post('/storage/StockIn/Edit', params)
+// 删除
+export const Storehouse_StockIn_del = (params: any) => $http.post('/storage/StockIn/Del', params)
+
 
 /**
  * 出库
@@ -125,6 +130,10 @@ export const Storehouse_StockOut_ListProduct = (params: any) => $http.post('/sto
 export const Storehouse_StockOut_Add = (params: any) => $http.post('/storage/StockOut/Add', params)
 // 详情
 export const Storehouse_StockOut_Get = (params: any) => $http.post('/storage/StockOut/Get', params)
+// 编辑
+export const Storehouse_StockOut_EditGet = (params: any) => $http.post('/storage/StockOut/Edit', params)
+// 删除
+export const Storehouse_StockOut_Del = (params: any) => $http.post('/storage/StockOut/Del', params)
 // 修改发货订单
 export const Storehouse_StockOut_Edit = (params: any) => $http.post('/storage/StockOut/Edit_Delivery', params)
 

+ 13 - 0
src/hooks/useDepot.ts

@@ -75,6 +75,19 @@ export interface InStoreageFormType {
   T_remark: string
 }
 
+
+export interface InStoreageFormTypes {
+    T_number: string
+    T_depot_id: any
+    T_product: any
+    T_project: any
+    T_date: string
+    T_remark: string
+    T_receive:string
+    T_receive_name:string
+
+  }
+
 export const delivery_type = [
   { name: '自送', id: 1 },
   { name: '自提', id: 2 },

+ 9 - 1
src/router/modules/staticRouter.ts

@@ -52,7 +52,7 @@ export const staticRouter: RouteRecordRaw[] = [
             name: 'PercentageDetail1',
             component: () => import('@/views/storehouse/sales/PercentageDetail1.vue'),
             meta: {
-              title: '提成详情1'
+              title: '提成详情'
             }
           },
           {
@@ -84,6 +84,14 @@ export const staticRouter: RouteRecordRaw[] = [
             }
           },
           {
+            path: '/InStorageEdit/:id',
+            name: 'InStorageEdit',
+            component: () => import('@/views/storehouse/inventory/InStorageEdit.vue'),
+            meta: {
+              title: '入库编辑'
+            }
+          },
+          {
             path: '/receiveOutStock',
             name: 'ReceiveOutStock',
             component: () => import('@/views/storehouse/outStock/ReceiveOutStock.vue'),

+ 2 - 0
src/views/Login.vue

@@ -77,10 +77,12 @@ const changeType = () => {
 <template>
   <div class="login">
     <div class="content">
+        
       <div class="logo">
         <img src="../assets/images/icon.png" />
       </div>
       <div class="title">
+        
         <h2>宝智达ERP系统</h2>
         <span>ShenZhen Baozhida Technology ERP. system</span>
       </div>

+ 21 - 8
src/views/storehouse/InventoryStatistics.vue

@@ -18,6 +18,9 @@ import { dayJs } from '@/utils/common'
 import ImageCom from '@/components/Image/index.vue'
 import { useTablePublic } from '@/hooks/useTablePublic'
 
+import InventoryStatisticsshowModel from "./modules/InventoryStatisticsshowModel.vue";
+
+
 const userInfo = ref({
     Id: '',
     T_name: ''
@@ -64,14 +67,16 @@ const detailColumns: ColumnProps[] = [
     { prop: 'T_beginning', label: '期初库存' },
     { prop: 'T_in', label: '入库' },
     { prop: 'T_out', label: '出库' },
-    { prop: 'T_ending', label: '期末库存' }
+    { prop: 'T_ending', label: '期末库存' },
+    { prop: 'operation', label: '关联项目', width: 200, fixed: 'right' }
 ]
 
 const searchHandle = () => {
     TableRef.value?.searchTable()
 }
 const dataCallback = (res: any) => {
-    return res.Data.Data.map((item: any) => {
+    let arr = res.Data.Data || []
+    return arr.map((item: any) => {
         if (item.T_depot_id === userInfo.value.Id) {
             item.T_depot_name = userInfo.value.T_name
         }
@@ -84,17 +89,18 @@ 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 (project_id?: string) => {
+
     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],
+        T_start_date: T_date_detail.value[0],
+        T_end_date: T_date_detail.value[1],
         T_product_id: ''
     }
+    console.log('导出',T_date_detail.value,params)
     if (typeof project_id === 'number') {
         params['T_product_id'] = project_id
     }
@@ -178,9 +184,9 @@ const dateDetailChange = (str: string[]) => {
 }
 onMounted(() => {
     getProductClassList()
-
     T_date.value = [dayJs().startOf('year').format('YYYY-MM-DD'), dayJs().format('YYYY-MM-DD')]
-    T_date_export.value = [...T_date.value]
+    T_date_detail.value = [...T_date.value]
+    console.log('初始化时间',T_date)
 })
 
 const getSalaryParams = (row: any) => {
@@ -190,6 +196,9 @@ const getSalaryParams = (row: any) => {
         initParam.T_depot_id = userInfo.value.Id
     }, 100)
 }
+
+
+
 </script>
 
 <template>
@@ -248,7 +257,7 @@ const getSalaryParams = (row: any) => {
                                             <template #reference>
                                                 <el-button type="success" @click="exportExcel">导出</el-button>
                                             </template>
-                                            <el-date-picker v-model="T_date_export" type="daterange" range-separator="~"
+                                            <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" />
                                             <div class="export-popover">
@@ -283,6 +292,7 @@ const getSalaryParams = (row: any) => {
                 <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>
@@ -291,6 +301,9 @@ const getSalaryParams = (row: any) => {
                         </el-col>
                     </el-row>
                 </template>
+                <template #right="{ row }">
+                    <InventoryStatisticsshowModel :rows="row"/>
+                </template>
             </TableBase>
         </Drawer>
     </div>

+ 34 - 20
src/views/storehouse/inventory/InStorage.vue

@@ -4,10 +4,13 @@ import { GlobalStore } from '@/stores/index'
 import { View,Edit,Delete } from '@element-plus/icons-vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import InStorageForm from './InStorageForm.vue'
+import InStorageEdit from './InStorageEdit.vue'
+
+
 import { ref, reactive } from 'vue'
 import TableBase from '@/components/TableBase/index.vue'
 import type { ColumnProps } from '@/components/TableBase/interface/index'
-import { Storehouse_StockIn_List } from '@/api/storehouse/index'
+import { Storehouse_StockIn_List,Storehouse_StockIn_del } from '@/api/storehouse/index'
 import { depotHooks } from '@/hooks/useDepot'
 
 const router = useRouter()
@@ -24,12 +27,27 @@ const columns: ColumnProps[] = [
 ]
 
 const InStorageFormRef = ref<InstanceType<typeof InStorageForm> | null>(null)
+const InStorageRef = ref()
+
 /**
  * 查看详情
  */
 const preview = (id: string) => {
   router.push({ name: 'InStorageDetail', params: { id } })
 }
+/**
+ * 编辑
+ */
+const previewEdit = (row: any) => {
+    // console.log('编辑',row,InStorageRef.value)
+  InStorageRef.value?.openDrawer()
+  InStorageRef.value?.getStorehouseContractGet(row.T_number)
+  for (let key in InStorageRef.value?.form) {  
+    if (row.hasOwnProperty(key)) InStorageRef.value.form[key] = row[key]; 
+  }
+//   console.log('111',InStorageRef.value?.form)
+
+}
 
 // 搜索
 const T_date = ref<string[]>([])
@@ -54,29 +72,23 @@ const openInStorageFormDrawer = () => InStorageFormRef.value?.openDrawer()
 /**
  * 删除
  */
-const deleteFun = async (row:any)=>{
+const deleteFun = (row:any)=>{
     ElMessageBox.confirm(
-    'proxy will permanently delete the file. Continue?',
-    'Warning',
+    '删除操作,是否立即删除?',
+    '删除',
     {
-      confirmButtonText: 'OK',
-      cancelButtonText: 'Cancel',
+      confirmButtonText: '立即删除',
+      cancelButtonText: '取消',
       type: 'warning',
       center: true,
     }
-  )
-    .then(() => {
-      ElMessage({
-        type: 'success',
-        message: 'Delete completed',
-      })
-    })
-    .catch(() => {
-      ElMessage({
-        type: 'info',
-        message: 'Delete canceled',
-      })
-    })
+  ).then(async () => {
+    const result:any = await Storehouse_StockIn_del({T_number:row})
+    if(result.Code==200){
+        ElMessage.success('删除成功')
+        TableRef.value?.searchTable()
+    }
+    }).catch(() => {})
 }
 
 
@@ -117,12 +129,14 @@ const { options } = depotHooks()
       </template>
       <template #right="{ row }">
         <el-button link type="success" size="small" :icon="View" @click="preview(row.T_number)">详情</el-button>
-        <el-button link type="success" size="small" :icon="Edit" @click="preview(row.T_number)">编辑</el-button>
+        <el-button link type="success" size="small" :icon="Edit" @click="previewEdit(row)">编辑</el-button>
         <el-button link type="danger" size="small" :icon="Delete" @click="deleteFun(row.T_number)">删除</el-button>
 
       </template>
     </TableBase>
     <InStorageForm ref="InStorageFormRef" :options="options" @onUpdateList="searchHandle" />
+    <InStorageEdit ref="InStorageRef" :options="options" @onUpdateList="searchHandle" />
+
   </div>
 </template>
 

+ 380 - 0
src/views/storehouse/inventory/InStorageEdit.vue

@@ -0,0 +1,380 @@
+<script setup lang="ts">
+import { ElMessage } from 'element-plus'
+import InStorageEditSn from './InStorageEditSn.vue'
+import { ref, reactive, nextTick } from 'vue'
+import { GlobalStore } from '@/stores/index'
+import Drawer from '@/components/Drawer/index.vue'
+import { InStoreageFormType } from '@/hooks/useDepot'
+import InStorageProduct from './InStorageProduct.vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { Delete, CirclePlus } from '@element-plus/icons-vue'
+import { Storehouse_StockIn_Edit,Storehouse_StockIn_Get } from '@/api/storehouse/index'
+import ImageCom from '@/components/Image/index.vue'
+
+
+const tableData = ref<any[]>([])
+const globalStore = GlobalStore()
+const formLabelWidth = ref('120px')
+const ruleFormRef = ref<FormInstance>()
+const drawerRef = ref<InstanceType<typeof Drawer> | null>(null)
+const drawerSnEditRef = ref<InstanceType<typeof InStorageEditSn> | null>(null)
+const drawerProductRef = ref<InstanceType<typeof InStorageProduct> | null>(null)
+
+const form = reactive<InStoreageFormType>({
+  T_number: '',
+  T_depot_id: '',
+  T_product: '',
+  T_date: '',
+  T_remark: ''
+})
+
+const validate_T_product = (rule: any, value: any, callback: any) => {
+    // console.log('验证',value)
+  if (value==undefined || value == '') {
+    callback(new Error('请填写产品数量'))
+  } else if (value.includes(null)) {
+    callback(new Error('请添加产品SN'))
+  } else {
+    callback()
+  }
+}
+
+const rules = reactive<FormRules>({
+  T_product: [{ validator: validate_T_product, trigger: 'blur' }],
+  T_depot_id: [{ required: true, message: '请选择仓库', trigger: 'blur' }],
+  T_date: [{ required: true, message: '请选择入库日期', trigger: 'blur' }]
+})
+
+const columns = [
+  { type: 'index', label: '序号', width: 80, align: 'center ' },
+  { label: '产品图片', prop: 'T_img', align: 'center ', name: 'T_img' },
+  { label: '产品名称', prop: 'T_name', align: 'center ' },
+  { label: '产品分类', prop: 'T_class_name', align: 'center ' },
+  { label: '产品型号', prop: 'T_number', align: 'center ', ellipsis: true },
+  { label: '产品规格', prop: 'T_spec', align: 'center ' },
+  { label: '是否关联SN', prop: 'T_relation_sn', align: 'center ', width: 120, name: 'T_relation_sn' },
+  { label: '*数量', prop: 'count', align: 'center ', name: 'count' },
+  { label: '*关联设备', prop: 'sn', align: 'center ', name: 'sn' },
+  { prop: 'operation', label: '操作', width: 80, fixed: 'right' }
+]
+
+const countBlurHandle = () => {
+  form.T_product = tableData.value.map(item => {
+    if (!item.count && item.T_relation_sn !== 1) return undefined
+    return `${item.Id},${item.count}|`
+  })
+}
+const getDeviceSnToProduct = () => {
+    const DeviceSnData: any = drawerSnEditRef.value?.getDeviceSn()
+    const isEmpty = determineSNorCount(DeviceSnData)
+    if (isEmpty && isEmpty === 'count') return [undefined]
+    if (isEmpty && isEmpty === 'sn') return [null]
+    let arr = [...tableData.value]
+    let mapArr:any = []
+    let flag = false
+    for (const item of arr) {
+        if(item.count==0){
+            flag = true
+            break;
+        }
+        mapArr.push(item.T_product_id + '-' + item.count + '-' +  (item.T_device_list?item.T_device_list.join(','):''))
+    }
+    return flag?undefined:mapArr.join('|')+'|'
+}
+/**
+ * 判断sn or 数量是否为空
+ */
+const determineSNorCount = (DeviceSnData: any) => {
+  for (const item of tableData.value) {
+    if (item.count<1 && item.T_relation_sn !== 1) return 'count'  //数量必大于0
+    // if (item.T_relation_sn === 1 && !DeviceSnData.get(item.Id) && !DeviceSnData.size) return 'sn'
+  }
+  return false
+}
+
+const AddInStorage = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  form.T_product = getDeviceSnToProduct()
+  formEl.validate(async valid => {
+    if (valid) {
+        console.log('提交',tableData.value,{
+        User_tokey: globalStore.GET_User_tokey,
+        ...form
+      })
+      const res: any = await Storehouse_StockIn_Edit({
+        User_tokey: globalStore.GET_User_tokey,
+        ...form
+      })
+      if (res.Code === 200) {
+        ElMessage.success('编辑成功!')
+        drawerProductRef.value?.clearSelection()
+        nextTick(() => {
+          emit('onUpdateList')
+          resetForm(ruleFormRef.value)
+          drawerRef.value?.closeDrawer()
+        })
+      }
+    }
+  })
+}
+
+const deleteProduct = (row: any) => {
+  tableData.value = tableData.value.filter(item => item.Id !== row.Id)
+
+  // 设置产品的选中
+  drawerProductRef.value?.selectTableChange(row)
+  // 删除设备得sn
+  drawerSnEditRef.value?.deleteDeviceSn(row.Id)
+}
+
+const callbackDrawer = (done: () => void) => {
+  resetForm(ruleFormRef.value)
+  done()
+}
+/**
+ * 重置表单
+ */
+const resetForm = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  drawerProductRef.value?.clearSelection()
+  drawerSnEditRef.value?.clearDeviceSn()
+  tableData.value = []
+  formEl.resetFields()
+}
+
+/**
+ * 添加产品
+ */
+const AddProductionDetailed = () => drawerProductRef.value?.openDrawer()
+/**
+ * 单选
+ */
+const ProductselectionChange = (row: any) => {
+    row.T_product_id = row.Id
+  const index = tableData.value.findIndex((item: any) => item.T_product_id === row.T_product_id)
+  if (index === -1) {
+    row.count = 0
+    tableData.value.push(row)
+  } else {
+    tableData.value.splice(index, 1)
+  }
+}
+/**
+ * 全选
+ */
+const ProductSelectionAllChange = (selection: any[]) => {
+  console.log('全选',selection)
+  let arr = [...selection]
+  arr.forEach((item:any)=>{
+    item.T_product_id = item.Id
+  })
+  tableData.value = selection
+}
+
+const getStorehouseContractGet = async (Id:any) => {
+  const res: any = await Storehouse_StockIn_Get({ User_tokey: globalStore.GET_User_tokey, T_number:Id })
+  if (res.Code === 200) {
+    let arr = res.Data.T_Product
+    arr.forEach((item:any)=>{
+        tableData.value.push({
+            Id: item.Id,
+            T_class_name:item.T_product_class_name,
+            T_img:item.T_product_img,
+            T_model:item.T_number,
+            T_name: item.T_product_name,
+            count: item.T_num,
+            T_relation_sn:item.T_product_relation_sn,
+            T_spec: item.T_product_spec,
+            T_device_list:item.T_device_list,
+            T_product_id:item.T_product_id
+        })
+    })
+  }
+}
+
+/**
+ * 添加sn 号
+ */
+ 
+const addDeviceSn = (obj: any) => {
+    drawerSnEditRef.value?.addDeviceSn(obj.Id,2)
+    drawerSnEditRef.value!.drawerSnRef?.openDrawer()
+    if(obj.T_device_list)drawerSnEditRef.value!.tableSnData = [...obj.T_device_list].map(item => ({ sn: item }));
+}
+/**
+ * 自动计算 count
+ */
+const autoGetCount = (length: number, id: number,arr:any) => {
+  tableData.value.forEach((item: any) => {
+    if (item.Id === id) {
+        item.T_device_list = arr
+      item.count = length
+    }
+  })
+}
+/**
+ * 关闭 取消
+ */
+const closeInStorage = () => {
+  resetForm(ruleFormRef.value)
+  drawerRef.value?.closeDrawer()
+}
+// 注册事件
+const emit = defineEmits<{ (event: 'onUpdateList'): void }>()
+
+// 接受props
+interface ItemType {
+  T_name: string
+  Id: number
+}
+interface PropsType {
+  options?: ItemType[]
+}
+const props = defineProps<PropsType>()
+/**
+ * 入库调用
+ */
+const openDrawer = () => drawerRef.value?.openDrawer()
+
+
+const disabledDate = (time:any)=> {  
+    const today = new Date();  
+    const isYear = today.getFullYear()//当前年
+    const isMonth = today.getMonth()+1//当前月
+    
+    const splitsYear = Number(form.T_date.split('-')[0])//已选年
+    const splitsMonth = Number(form.T_date.split('-')[1])//已选月
+    
+
+    if(isYear!==splitsYear || isMonth !==splitsMonth){
+        return true
+    }else{
+        const timeYear = time.getFullYear();  
+        const timeMonth = time.getMonth()+1; 
+        return timeYear !== isYear || timeMonth !== isMonth;   
+    }
+}
+
+defineExpose({
+  openDrawer,getStorehouseContractGet,form
+})
+</script>
+<template>
+  <div class="inStorage-form">
+    <Drawer ref="drawerRef" :handleClose="callbackDrawer" size="80%">
+      <template #header="{ params }">
+        <h4 :id="params.titleId" :class="params.titleClass">编辑</h4>
+      </template>
+      <el-form ref="ruleFormRef" :model="form" :rules="rules">
+        <el-form-item label="入库单号:" :label-width="formLabelWidth" prop="T_number">
+          <el-input v-model="form.T_number" type="text" :disabled="true" placeholder="系统自动生成" class="w-50" />
+        </el-form-item>
+        <el-form-item label="入库仓库:" :label-width="formLabelWidth" prop="T_depot_id">
+          <el-select v-model="form.T_depot_id" class="w-50" :disabled="true" clearable placeholder="请选择入库仓库~">
+            <el-option v-for="item in props.options" :key="item.Id" :label="item.T_name" :value="item.Id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="入库日期:" :label-width="formLabelWidth" prop="T_date">
+          <el-date-picker
+            class="my-date-picker"
+            style="width: 21.5rem"
+            v-model="form.T_date"
+            type="date"
+            placeholder="选择日期"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+            :disabledDate="disabledDate"
+            :clearable="false"
+          />
+        </el-form-item>
+        <el-form-item label="入库明细:" :label-width="formLabelWidth" prop="T_product">
+          <el-table
+            :data="tableData"
+            style="width: 100%"
+            border
+            stripe
+            :header-cell-style="{
+              background: '#dedfe0',
+              height: '50px'
+            }"
+          >
+            <template v-for="item in columns" :key="item.prop">
+              <el-table-column show-overflow-tooltip v-bind="item" v-if="item.fixed !== 'right' && !item.ellipsis">
+                <template #header v-if="item.prop === 'count' || item.prop === 'sn'">
+                  <span style="color: red">{{ item.label }}</span>
+                </template>
+                <template #default="{ row }" v-if="item.prop === item.name">
+                  <el-input
+                    v-if="item.prop === 'count' && row.T_relation_sn !== 1"
+                    v-model.number="row.count"
+                    type="text"
+                    autocomplete="off"
+                    @blur="countBlurHandle"
+                  />
+                  <div v-if="item.prop === 'sn'">
+                    <el-button
+                      v-if="row.T_relation_sn === 1"
+                      link
+                      type="primary"
+                      size="small"
+                      :icon="CirclePlus"
+                      @click="addDeviceSn(row)"
+                      >添加</el-button
+                    >
+                    <span v-else>-</span>
+                  </div>
+                  
+                  <span v-if="item.prop === 'T_relation_sn'">
+                    <el-tag v-if="row.T_relation_sn === 1" effect="dark">是</el-tag>
+                    <el-tag v-else type="success" effect="dark">否</el-tag>
+                  </span>
+                  <ImageCom v-if="item.prop == 'T_img'" :src="row.T_img" />
+                </template>
+              </el-table-column>
+              <el-table-column show-overflow-tooltip v-if="item.ellipsis && item.prop === 'T_model'" v-bind="item">
+                <template #default="{ row }">
+                  <el-tooltip effect="dark" :content="row.T_model" placement="bottom">
+                    {{ row.T_model }}
+                  </el-tooltip>
+                </template>
+              </el-table-column>
+              <el-table-column v-bind="item" v-if="item.fixed === 'right'">
+                <template #default="{ row }">
+                  <el-button link type="danger" size="small" :icon="Delete" @click="deleteProduct(row)">删除</el-button>
+                </template>
+              </el-table-column>
+            </template>
+            <template #append>
+              <el-button type="primary" @click="AddProductionDetailed">
+                <el-icon><Plus /></el-icon><span style="margin-left: 6px">添加产品</span>
+              </el-button>
+            </template>
+          </el-table>
+        </el-form-item>
+        <el-form-item label="备注:" :label-width="formLabelWidth" prop="T_remark">
+          <el-input
+            v-model="form.T_remark"
+            :autosize="{ minRows: 4, maxRows: 6 }"
+            type="textarea"
+            placeholder="请输入备注信息"
+          />
+        </el-form-item>
+        <div class="btn">
+          <el-divider>
+            <el-button @click="closeInStorage">取消</el-button>
+            <el-button color="#626aef" @click="AddInStorage(ruleFormRef)">提交</el-button>
+          </el-divider>
+        </div>
+        <InStorageEditSn ref="drawerSnEditRef" @onCount="autoGetCount" />
+        <InStorageProduct
+          ref="drawerProductRef"
+          @ontableData="ProductselectionChange"
+          @ontableDataAll="ProductSelectionAllChange"
+        />
+      </el-form>
+    </Drawer>
+  </div>
+</template>
+<style scoped lang="scss">
+@import './index.scss';
+</style>

+ 211 - 0
src/views/storehouse/inventory/InStorageEditSn.vue

@@ -0,0 +1,211 @@
+<script setup lang="ts">
+import { ref, reactive, nextTick } from 'vue'
+import * as XLSX from 'xlsx'; 
+import { Delete } from '@element-plus/icons-vue'
+import Drawer from '@/components/Drawer/index.vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { Storehouse_Device_Check } from '@/api/storehouse/index'
+import { ElMessage } from 'element-plus'
+type Fn = () => void
+interface FormSnType {
+  Id?: number
+  sn: string
+  type?: number
+}
+
+const snTable = ref()
+const SNDataMap = new Map<number, FormSnType[]>()
+const tableSnData = ref<FormSnType[]>([])
+const ruleSnFormRef = ref<FormInstance>()
+const drawerSnRef = ref<InstanceType<typeof Drawer> | null>(null)
+
+const snColumns = [
+  { type: 'index', label: '序号', width: 80, align: 'center ' },
+  { label: 'SN', prop: 'sn', align: 'center ' },
+  { prop: 'operation', label: '操作', width: 80, fixed: 'right' }
+]
+const formSn = reactive<FormSnType>({
+  Id: undefined,
+  sn: '',
+  type: undefined
+})
+const rulesSn = reactive<FormRules>({
+  sn: [{ required: true, message: '请输入SN号', trigger: 'blur' }]
+})
+
+const addSn = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  formEl.validate(async valid => {
+    if (valid) {
+        if (formSn.sn.length === 16 || formSn.sn.length === 24) {  
+            if(formSn.sn.length==24){
+                formSn.sn = formSn.sn.substring(2, formSn.sn.length - 6)
+            }  
+            const res: any = await Storehouse_Device_Check({ T_sn: formSn.sn, T_type: formSn.type })
+            if (res.Code === 200) {
+                tableSnData.value.unshift({ sn: formSn.sn })
+                tableSnData.value = tableSnData.value.filter((value, index, self) => {  //去重
+                    return self.findIndex(t => (t.sn === value.sn)) === index;  
+                });  
+                nextTick(() => {
+                    resetSnForm(ruleSnFormRef.value)
+                })
+            }
+        } else {  
+            ElMessage.error('扫描设备异常')
+            nextTick(() => {
+                resetSnForm(ruleSnFormRef.value)
+            })
+            return 
+        }  
+    }
+  })
+}
+const deleteSn = (row: any) => {
+  const index = tableSnData.value.findIndex((item: any) => row.sn === item.sn)
+  tableSnData.value.splice(index, 1)
+}
+
+const callbackSnDrawer = (done: Fn) => {
+  SNDataMap.set(formSn.Id as number, tableSnData.value)
+  let arrs = tableSnData.value.map((item:any) => item.sn);
+  emit('onCount', SNDataMap.get(formSn.Id as number)?.length, formSn.Id as number,arrs)
+  done()
+  nextTick(() => {
+    resetSnForm(ruleSnFormRef.value)
+    tableSnData.value = []
+  })
+}
+
+const emit = defineEmits<{ (event: 'onCount', value: any, id: number,mapArr:any): void }>()
+
+const resetSnForm = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  formEl.resetFields()
+}
+
+const addDeviceSn = (id: number, type: number) => {
+    console.log('1212',id,type)
+  formSn.Id = id
+  formSn.type = type
+  if (SNDataMap.has(id)) {
+    tableSnData.value = SNDataMap.get(id) as FormSnType[]
+  } else {
+    SNDataMap.set(id, [])
+  }
+  drawerSnRef.value?.openDrawer()
+}
+
+const drawer = ref(false)
+/**
+ * 导入xlsx
+ */
+
+// 组件状态变化的回调
+const uploadExcelFile = (event:any) => {
+    const files = event.target.files;  
+      if (files.length === 0) return;  
+      const file = files[0];  
+      const reader = new FileReader();  
+      reader.onload = (e:any) => {  
+        const data = new Uint8Array(e.target.result);  
+        const workbook = XLSX.read(data, { type: 'array' });  
+        // 假设我们知道第一个工作表包含我们需要的数据  
+        const firstSheetName = workbook.SheetNames[0];  
+        const worksheet:any = workbook.Sheets[firstSheetName];  
+        // 假设我们知道 t_sn 是第一列  
+        const tSnColumn = 'A'; // 或者使用 XLSX.utils.decode_col(columnNumber) 来从列号获取列名  
+        const columnNumber = XLSX.utils.decode_col(tSnColumn); // 但实际上我们可能直接知道列号,如 0  
+        // 使用 sheet_to_json 但只选择我们需要的列  
+        const json = XLSX.utils.sheet_to_json(worksheet, { header: 1, range: XLSX.utils.decode_range(worksheet['!ref']) });
+        // 如果表头不是第一行,或者你不想要表头,可以调整 header 选项  
+        // 提取 t_sn 列的数据(假设表头在第一行,且 t_sn 是第一列)  
+        let tSnData:any = json.map((row:any) => row[0]); // 假设 t_sn 是第一列,所以使用 row[0]  
+        let snArray = tSnData.map((sn:any) => ({ sn: sn }));
+        snArray = snArray.slice(1)
+        let arrs = [...tableSnData.value,...snArray]
+        // 去重
+        let seen = new Set();  
+        tableSnData.value = arrs.filter((item:any) => {  
+            return seen.has(item.sn) ? false : seen.add(item.sn) || true;  
+        }); 
+
+        console.log('打印',tableSnData.value)
+        // 注意:如果 t_sn 列不是第一列,你需要调整索引号  
+        // 例如,如果 t_sn 是第三列,则使用 row[2]  
+      };  
+  
+      reader.readAsArrayBuffer(file);
+
+};
+const getDeviceSn = () => SNDataMap
+const clearDeviceSn = () => SNDataMap.clear()
+const deleteDeviceSn = (id: number) => SNDataMap.delete(id)
+defineExpose({
+  getDeviceSn,
+  addDeviceSn,
+  clearDeviceSn,
+  deleteDeviceSn,tableSnData,drawerSnRef
+})
+</script>
+
+<template>
+  <Drawer ref="drawerSnRef" :handleClose="callbackSnDrawer" size="50%">
+    <el-card class="box-card" shadow="never">
+      <template #header>
+        <div class="sn-header">
+          <el-form ref="ruleSnFormRef" :model="formSn" :rules="rulesSn">
+            <el-form-item label="SN:" label-width="120px" prop="sn">
+              <el-input v-model="formSn.sn" type="text" placeholder="请输入SN" @keyup.enter="addSn(ruleSnFormRef)"   class="w-50" />
+            </el-form-item>
+          </el-form>
+          <el-button type="primary" @click="addSn(ruleSnFormRef)">添加</el-button>
+          <el-button type="success" @click="drawer = true">导入xlsx</el-button>
+        </div>
+      </template>
+      <el-table
+        ref="snTable"
+        :data="tableSnData"
+        style="width: 100%; height: 99%"
+        :header-cell-style="{
+          background: '#dedfe0',
+          height: '50px'
+        }"
+      >
+        <template v-for="item in snColumns" :key="item">
+          <el-table-column v-if="item.type === 'index'" v-bind="item" />
+          <el-table-column show-overflow-tooltip v-if="item.prop" align="center" v-bind="item">
+            <template #default="{ row }">
+              <el-button
+                v-if="item.prop === 'operation'"
+                link
+                type="danger"
+                size="small"
+                :icon="Delete"
+                @click="deleteSn(row)"
+                >删除</el-button
+              >
+            </template>
+          </el-table-column>
+        </template>
+      </el-table>
+    </el-card>
+    <el-drawer v-model="drawer" title="导入xlsx" size="50%" :destroy-on-close="true">
+        <input type="file" @change="uploadExcelFile" accept=".xlsx, .xls" /> 
+    </el-drawer>
+  </Drawer>
+
+</template>
+
+<style scoped lang="scss">
+.box-card {
+  height: 100%;
+  :deep(.el-card__body) {
+    height: calc(100% - 70px);
+  }
+  .sn-header {
+    display: flex;
+    justify-content: end;
+  }
+}
+</style>

+ 42 - 0
src/views/storehouse/modules/InventoryStatisticsshowModel.vue

@@ -0,0 +1,42 @@
+<template>
+    <div class="">
+        <el-popover placement="left" trigger="manual" :width="500" :teleported="true"
+            v-model:visible="visibles">
+            <template #reference>
+                <el-button @click="togglePopover">查看</el-button>
+            </template>
+            <el-table :data="gridData" border max-height="700">
+                <el-table-column property="text" label="关联项目" />
+            </el-table>
+        </el-popover>
+    </div>
+</template>
+
+<script setup lang='ts'>
+import { ref, computed } from 'vue';
+import { ElMessage } from 'element-plus'
+const props = defineProps({
+    rows:{
+        type:Object,
+        default: () => {},
+    }
+})
+
+const gridData = ref([])
+
+const visibles = ref(false)
+const togglePopover = (data:any)=>{
+    let project = props.rows.T_project
+    if(project){
+        const splitData = project.split('|')
+        gridData.value = splitData.map((item:any)=> {return {text:item}})
+        visibles.value = true
+    }else{
+        ElMessage.info('没有关联项目')
+    }
+   
+}
+</script>
+<style lang="scss">
+/* @import url(); 引入css类 */
+</style>

+ 43 - 5
src/views/storehouse/outStock/OutStock.vue

@@ -1,17 +1,17 @@
 <script setup lang="ts">
-import { ElMessage } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
 import { useRouter } from 'vue-router'
 import { GlobalStore } from '@/stores/index'
-import { View, Van } from '@element-plus/icons-vue'
+import { View, Van,Edit,Delete } from '@element-plus/icons-vue'
 import Drawer from '@/components/Drawer/index.vue'
 import type { FormInstance, FormRules } from 'element-plus'
 import { validate_T_phone } from '@/views/account/users/components/relus'
 import { ref, reactive, nextTick } from 'vue'
 import TableBase from '@/components/TableBase/index.vue'
 import type { ColumnProps } from '@/components/TableBase/interface/index'
-import { Storehouse_StockOut_List, Storehouse_StockOut_Edit } from '@/api/storehouse/index'
+import { Storehouse_StockOut_List, Storehouse_StockOut_Edit,Storehouse_StockOut_Del } from '@/api/storehouse/index'
 import { depotHooks, delivery_type } from '@/hooks/useDepot'
-
+import InStorageEdit from './modules/InStorageEdit.vue'
 const router = useRouter()
 const formLabelWidth = ref('120px')
 const globalStore = GlobalStore()
@@ -28,7 +28,7 @@ const columns: ColumnProps[] = [
   { prop: 'T_project', label: '关联项目' },
   { prop: 'T_type', label: '出库类型', name: 'T_type' },
   { prop: 'T_contract_number', label: '合同编号' },
-  { prop: 'operation', label: '操作', width: 170, fixed: 'right', align: 'left ' }
+  { prop: 'operation', label: '操作', width: 250, fixed: 'right', align: 'left ' }
 ]
 
 const form = reactive({
@@ -54,7 +54,18 @@ const rules = reactive<FormRules>({
 const preview = (number: string) => {
   router.push({ name: 'OutStockDetail', params: { number } })
 }
+const InStorageRef = ref()
+/**
+ * 编辑
+ */
+ const previewEdit = (row: any) => {
+  InStorageRef.value?.openDrawer()
+  InStorageRef.value?.getStorehouseContractGet(row.T_number)
+  for (let key in InStorageRef.value?.form) {  
+    if (row.hasOwnProperty(key)) InStorageRef.value.form[key] = row[key]; 
+  }
 
+}
 // 搜索
 const T_date = ref<string[]>([])
 const initParam = reactive({
@@ -100,6 +111,30 @@ const resetForm = (formEl: FormInstance | undefined) => {
   if (!formEl) return
   formEl.resetFields()
 }
+
+/**
+ * 删除
+ */
+ const deleteFun = (row:any)=>{
+    ElMessageBox.confirm(
+    '删除操作,是否立即删除?',
+    '删除',
+    {
+      confirmButtonText: '立即删除',
+      cancelButtonText: '取消',
+      type: 'warning',
+      center: true,
+    }
+  ).then(async () => {
+    const result:any = await Storehouse_StockOut_Del({T_number:row})
+    if(result.Code==200){
+        ElMessage.success('删除成功')
+        TableRef.value?.searchTable()
+    }
+    }).catch(() => {})
+}
+
+
 // 拿到仓库列表
 const { options } = depotHooks()
 </script>
@@ -142,6 +177,8 @@ const { options } = depotHooks()
       </template>
       <template #right="{ row }">
         <el-button link type="success" size="small" :icon="View" @click="preview(row.T_number)">详情</el-button>
+        <el-button link type="success" size="small" :icon="Edit" @click="previewEdit(row)">编辑</el-button>
+        <el-button link type="danger" size="small" :icon="Delete" @click="deleteFun(row.T_number)">删除</el-button>
         <el-button
           v-if="row.T_type !== 1"
           link
@@ -200,6 +237,7 @@ const { options } = depotHooks()
         </div>
       </el-form>
     </Drawer>
+    <InStorageEdit ref="InStorageRef" :options="options" @onUpdateList="searchHandle" />
   </div>
 </template>
 

+ 399 - 0
src/views/storehouse/outStock/modules/InStorageEdit.vue

@@ -0,0 +1,399 @@
+<script setup lang="ts">
+import { ElMessage } from 'element-plus'
+import InStorageEditSn from './InStorageEditSn.vue'
+import { ref, reactive, nextTick } from 'vue'
+import { GlobalStore } from '@/stores/index'
+import Drawer from '@/components/Drawer/index.vue'
+import { InStoreageFormTypes } from '@/hooks/useDepot'
+import InStorageProduct from './InStorageProduct.vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { Delete, CirclePlus } from '@element-plus/icons-vue'
+import { Storehouse_StockOut_EditGet,Storehouse_StockOut_Get } from '@/api/storehouse/index'
+import ImageCom from '@/components/Image/index.vue'
+import ReceiveUser from '../receiveUser.vue'
+
+const tableData = ref<any[]>([])
+const globalStore = GlobalStore()
+const formLabelWidth = ref('120px')
+const ruleFormRef = ref<FormInstance>()
+const drawerRef = ref<InstanceType<typeof Drawer> | null>(null)
+const drawerSnEditRef = ref<InstanceType<typeof InStorageEditSn> | null>(null)
+const drawerProductRef = ref<InstanceType<typeof InStorageProduct> | null>(null)
+
+const form = reactive<InStoreageFormTypes>({
+  T_number: '',
+  T_depot_id: '',
+  T_product: '',//明细拼接
+  T_project:'',//关联项目
+  T_date: '',
+  T_receive:'',
+  T_receive_name:'',
+  T_remark: '',
+
+})
+
+const validate_T_product = (rule: any, value: any, callback: any) => {
+    // console.log('验证',value)
+  if (value==undefined || value == '') {
+    callback(new Error('请填写产品数量'))
+  } else if (value.includes(null)) {
+    callback(new Error('请添加产品SN'))
+  } else {
+    callback()
+  }
+}
+
+const rules = reactive<FormRules>({
+  T_product: [{ validator: validate_T_product, trigger: 'blur' }],
+  T_depot_id: [{ required: true, message: '请选择仓库', trigger: 'blur' }],
+  T_date: [{ required: true, message: '请选择出库日期', trigger: 'blur' }]
+})
+
+const columns = [
+  { type: 'index', label: '序号', width: 80, align: 'center ' },
+  { label: '产品图片', prop: 'T_img', align: 'center ', name: 'T_img' },
+  { label: '产品名称', prop: 'T_name', align: 'center ' },
+  { label: '产品分类', prop: 'T_class_name', align: 'center ' },
+  { label: '产品型号', prop: 'T_number', align: 'center ', ellipsis: true },
+  { label: '产品规格', prop: 'T_spec', align: 'center ' },
+  { label: '是否关联SN', prop: 'T_relation_sn', align: 'center ', width: 120, name: 'T_relation_sn' },
+  { label: '*数量', prop: 'count', align: 'center ', name: 'count' },
+  { label: '*关联设备', prop: 'sn', align: 'center ', name: 'sn' },
+  { prop: 'operation', label: '操作', width: 80, fixed: 'right' }
+]
+
+const countBlurHandle = () => {
+  form.T_product = tableData.value.map(item => {
+    if (!item.count && item.T_relation_sn !== 1) return undefined
+    return `${item.Id},${item.count}|`
+  })
+}
+const getDeviceSnToProduct = () => {
+    const DeviceSnData: any = drawerSnEditRef.value?.getDeviceSn()
+    const isEmpty = determineSNorCount(DeviceSnData)
+    if (isEmpty && isEmpty === 'count') return [undefined]
+    if (isEmpty && isEmpty === 'sn') return [null]
+    let arr = [...tableData.value]
+    let mapArr:any = []
+    let flag = false
+    for (const item of arr) {
+        if(item.count==0){
+            flag = true
+            break;
+        }
+        mapArr.push(item.T_product_id + '-' + item.count + '-' +  (item.T_device_list?item.T_device_list.join(','):''))
+    }
+    return flag?undefined:mapArr.join('|')+'|'
+}
+/**
+ * 判断sn or 数量是否为空
+ */
+const determineSNorCount = (DeviceSnData: any) => {
+  for (const item of tableData.value) {
+    if (item.count<1 && item.T_relation_sn !== 1) return 'count'  //数量必大于0
+    // if (item.T_relation_sn === 1 && !DeviceSnData.get(item.Id) && !DeviceSnData.size) return 'sn'
+  }
+  return false
+}
+
+const AddInStorage = (formEl: FormInstance | undefined) => {
+    
+  if (!formEl) return
+  form.T_product = getDeviceSnToProduct()
+  formEl.validate(async valid => {
+    if (valid) {
+      const res: any = await Storehouse_StockOut_EditGet({
+        ...form
+      })
+      if (res.Code === 200) {
+        ElMessage.success('编辑成功!')
+        drawerProductRef.value?.clearSelection()
+        nextTick(() => {
+          emit('onUpdateList')
+          resetForm(ruleFormRef.value)
+          drawerRef.value?.closeDrawer()
+        })
+      }
+    }
+  })
+}
+
+const deleteProduct = (row: any) => {
+  tableData.value = tableData.value.filter(item => item.Id !== row.Id)
+
+  // 设置产品的选中
+  drawerProductRef.value?.selectTableChange(row)
+  // 删除设备得sn
+  drawerSnEditRef.value?.deleteDeviceSn(row.Id)
+}
+
+const callbackDrawer = (done: () => void) => {
+  resetForm(ruleFormRef.value)
+  done()
+}
+/**
+ * 重置表单
+ */
+const resetForm = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  drawerProductRef.value?.clearSelection()
+  drawerSnEditRef.value?.clearDeviceSn()
+  tableData.value = []
+  formEl.resetFields()
+}
+
+/**
+ * 添加产品
+ */
+const AddProductionDetailed = () => drawerProductRef.value?.openDrawer()
+/**
+ * 单选
+ */
+const ProductselectionChange = (row: any) => {
+    row.T_product_id = row.Id
+  const index = tableData.value.findIndex((item: any) => item.T_product_id === row.T_product_id)
+  if (index === -1) {
+    row.count = 0
+    tableData.value.push(row)
+  } else {
+    tableData.value.splice(index, 1)
+  }
+}
+/**
+ * 全选
+ */
+const ProductSelectionAllChange = (selection: any[]) => {
+  console.log('全选',selection)
+  let arr = [...selection]
+  arr.forEach((item:any)=>{
+    item.T_product_id = item.Id
+  })
+  tableData.value = selection
+}
+
+const getStorehouseContractGet = async (Id:any) => {
+  const res: any = await Storehouse_StockOut_Get({ User_tokey: globalStore.GET_User_tokey, T_number:Id })
+  if (res.Code === 200) {
+    let arr = res.Data.T_Product
+    arr.forEach((item:any)=>{
+        tableData.value.push({
+            Id: item.Id,
+            T_class_name:item.T_product_class_name,
+            T_img:item.T_product_img,
+            T_model:item.T_number,
+            T_name: item.T_product_name,
+            count: item.T_num,
+            T_relation_sn:item.T_product_relation_sn,
+            T_spec: item.T_product_spec,
+            T_device_list:item.T_device_list,
+            T_product_id:item.T_product_id
+        })
+    })
+  }
+}
+
+/**
+ * 添加sn 号
+ */
+ 
+const addDeviceSn = (obj: any) => {
+    drawerSnEditRef.value?.addDeviceSn(obj.Id,2)
+    drawerSnEditRef.value!.drawerSnRef?.openDrawer()
+    if(obj.T_device_list)drawerSnEditRef.value!.tableSnData = [...obj.T_device_list].map(item => ({ sn: item }));
+}
+/**
+ * 自动计算 count
+ */
+const autoGetCount = (length: number, id: number,arr:any) => {
+  tableData.value.forEach((item: any) => {
+    if (item.Id === id) {
+        item.T_device_list = arr
+      item.count = length
+    }
+  })
+}
+/**
+ * 关闭 取消
+ */
+const closeInStorage = () => {
+  resetForm(ruleFormRef.value)
+  drawerRef.value?.closeDrawer()
+}
+// 注册事件
+const emit = defineEmits<{ (event: 'onUpdateList'): void }>()
+
+// 接受props
+interface ItemType {
+  T_name: string
+  Id: number
+}
+interface PropsType {
+  options?: ItemType[]
+}
+const props = defineProps<PropsType>()
+/**
+ * 出库调用
+ */
+const openDrawer = () => drawerRef.value?.openDrawer()
+
+
+const disabledDate = (time:any)=> {  
+    const today = new Date();  
+    const isYear = today.getFullYear()//当前年
+    const isMonth = today.getMonth()+1//当前月
+    
+    const splitsYear = Number(form.T_date.split('-')[0])//已选年
+    const splitsMonth = Number(form.T_date.split('-')[1])//已选月
+    
+
+    if(isYear!==splitsYear || isMonth !==splitsMonth){
+        return true
+    }else{
+        const timeYear = time.getFullYear();  
+        const timeMonth = time.getMonth()+1; 
+        return timeYear !== isYear || timeMonth !== isMonth;   
+    }
+}
+
+const receiveUserdialog = ref<InstanceType<typeof ReceiveUser> | null>(null)
+/**
+ * 选择经办人
+ */
+ const selectApprover = () => receiveUserdialog.value?.openDrawer()
+const getReceiveInfo = ({ T_uuid, T_name }: { T_uuid: string; T_name: string }) => {
+  form.T_receive_name = T_name
+  form.T_receive = T_uuid
+}
+
+defineExpose({
+  openDrawer,getStorehouseContractGet,form
+})
+</script>
+<template>
+  <div class="inStorage-form">
+    <Drawer ref="drawerRef" :handleClose="callbackDrawer" size="80%">
+      <template #header="{ params }">
+        <h4 :id="params.titleId" :class="params.titleClass">编辑</h4>
+      </template>
+      <el-form ref="ruleFormRef" :model="form" :rules="rules">
+        <el-form-item label="出库单号:" :label-width="formLabelWidth" prop="T_number">
+          <el-input v-model="form.T_number" type="text" :disabled="true" placeholder="系统自动生成" class="w-50" />
+        </el-form-item>
+        <el-form-item label="出库仓库:" :label-width="formLabelWidth" prop="T_depot_id">
+          <el-select v-model="form.T_depot_id" class="w-50" :disabled="true" clearable placeholder="请选择出库仓库~">
+            <el-option v-for="item in props.options" :key="item.Id" :label="item.T_name" :value="item.Id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="出库日期:" :label-width="formLabelWidth" prop="T_date">
+          <el-date-picker
+            class="my-date-picker"
+            style="width: 21.5rem"
+            v-model="form.T_date"
+            type="date"
+            placeholder="选择日期"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+            :disabledDate="disabledDate"
+            :clearable="false"
+          />
+        </el-form-item>
+        <el-form-item label="关联项目:" :label-width="formLabelWidth" prop="T_project">
+          <el-input v-model="form.T_project" type="text" placeholder="关联项目" class="w-50" />
+        </el-form-item>
+
+        <el-form-item label="经办人:" :label-width="formLabelWidth" prop="T_number">
+            <el-input v-model="form.T_receive_name" placeholder="请选择经办人" class="w-50" @focus="selectApprover" />
+        </el-form-item>
+
+        <el-form-item label="出库明细:" :label-width="formLabelWidth" prop="T_product">
+          <el-table
+            :data="tableData"
+            style="width: 100%"
+            border
+            stripe
+            :header-cell-style="{
+              background: '#dedfe0',
+              height: '50px'
+            }"
+          >
+            <template v-for="item in columns" :key="item.prop">
+              <el-table-column show-overflow-tooltip v-bind="item" v-if="item.fixed !== 'right' && !item.ellipsis">
+                <template #header v-if="item.prop === 'count' || item.prop === 'sn'">
+                  <span style="color: red">{{ item.label }}</span>
+                </template>
+                <template #default="{ row }" v-if="item.prop === item.name">
+                  <el-input
+                    v-if="item.prop === 'count' && row.T_relation_sn !== 1"
+                    v-model.number="row.count"
+                    type="text"
+                    autocomplete="off"
+                    @blur="countBlurHandle"
+                  />
+                  <div v-if="item.prop === 'sn'">
+                    <el-button
+                      v-if="row.T_relation_sn === 1"
+                      link
+                      type="primary"
+                      size="small"
+                      :icon="CirclePlus"
+                      @click="addDeviceSn(row)"
+                      >添加</el-button
+                    >
+                    <span v-else>-</span>
+                  </div>
+                  
+                  <span v-if="item.prop === 'T_relation_sn'">
+                    <el-tag v-if="row.T_relation_sn === 1" effect="dark">是</el-tag>
+                    <el-tag v-else type="success" effect="dark">否</el-tag>
+                  </span>
+                  <ImageCom v-if="item.prop == 'T_img'" :src="row.T_img" />
+                </template>
+              </el-table-column>
+              <el-table-column show-overflow-tooltip v-if="item.ellipsis && item.prop === 'T_model'" v-bind="item">
+                <template #default="{ row }">
+                  <el-tooltip effect="dark" :content="row.T_model" placement="bottom">
+                    {{ row.T_model }}
+                  </el-tooltip>
+                </template>
+              </el-table-column>
+              <el-table-column v-bind="item" v-if="item.fixed === 'right'">
+                <template #default="{ row }">
+                  <el-button link type="danger" size="small" :icon="Delete" @click="deleteProduct(row)">删除</el-button>
+                </template>
+              </el-table-column>
+            </template>
+            <template #append>
+              <el-button type="primary" @click="AddProductionDetailed">
+                <el-icon><Plus /></el-icon><span style="margin-left: 6px">添加产品</span>
+              </el-button>
+            </template>
+          </el-table>
+        </el-form-item>
+        <el-form-item label="备注:" :label-width="formLabelWidth" prop="T_remark">
+          <el-input
+            v-model="form.T_remark"
+            :autosize="{ minRows: 4, maxRows: 6 }"
+            type="textarea"
+            placeholder="请输入备注信息"
+          />
+        </el-form-item>
+        <div class="btn">
+          <el-divider>
+            <el-button @click="closeInStorage">取消</el-button>
+            <el-button color="#626aef" @click="AddInStorage(ruleFormRef)">提交</el-button>
+          </el-divider>
+        </div>
+        <InStorageEditSn ref="drawerSnEditRef" @onCount="autoGetCount" />
+        <InStorageProduct
+          ref="drawerProductRef"
+          @ontableData="ProductselectionChange"
+          @ontableDataAll="ProductSelectionAllChange"
+        />
+      </el-form>
+    </Drawer>
+    <ReceiveUser ref="receiveUserdialog" :dept_leader="0" @onUserInfo="getReceiveInfo" title="选择经办人" />
+
+  </div>
+</template>
+<style scoped lang="scss">
+</style>

+ 211 - 0
src/views/storehouse/outStock/modules/InStorageEditSn.vue

@@ -0,0 +1,211 @@
+<script setup lang="ts">
+import { ref, reactive, nextTick } from 'vue'
+import * as XLSX from 'xlsx'; 
+import { Delete } from '@element-plus/icons-vue'
+import Drawer from '@/components/Drawer/index.vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { Storehouse_Device_Check } from '@/api/storehouse/index'
+import { ElMessage } from 'element-plus'
+type Fn = () => void
+interface FormSnType {
+  Id?: number
+  sn: string
+  type?: number
+}
+
+const snTable = ref()
+const SNDataMap = new Map<number, FormSnType[]>()
+const tableSnData = ref<FormSnType[]>([])
+const ruleSnFormRef = ref<FormInstance>()
+const drawerSnRef = ref<InstanceType<typeof Drawer> | null>(null)
+
+const snColumns = [
+  { type: 'index', label: '序号', width: 80, align: 'center ' },
+  { label: 'SN', prop: 'sn', align: 'center ' },
+  { prop: 'operation', label: '操作', width: 80, fixed: 'right' }
+]
+const formSn = reactive<FormSnType>({
+  Id: undefined,
+  sn: '',
+  type: undefined
+})
+const rulesSn = reactive<FormRules>({
+  sn: [{ required: true, message: '请输入SN号', trigger: 'blur' }]
+})
+
+const addSn = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  formEl.validate(async valid => {
+    if (valid) {
+        if (formSn.sn.length === 16 || formSn.sn.length === 24) {  
+            if(formSn.sn.length==24){
+                formSn.sn = formSn.sn.substring(2, formSn.sn.length - 6)
+            }  
+            const res: any = await Storehouse_Device_Check({ T_sn: formSn.sn, T_type: formSn.type })
+            if (res.Code === 200) {
+                tableSnData.value.unshift({ sn: formSn.sn })
+                tableSnData.value = tableSnData.value.filter((value, index, self) => {  //去重
+                    return self.findIndex(t => (t.sn === value.sn)) === index;  
+                });  
+                nextTick(() => {
+                    resetSnForm(ruleSnFormRef.value)
+                })
+            }
+        } else {  
+            ElMessage.error('扫描设备异常')
+            nextTick(() => {
+                resetSnForm(ruleSnFormRef.value)
+            })
+            return 
+        }  
+    }
+  })
+}
+const deleteSn = (row: any) => {
+  const index = tableSnData.value.findIndex((item: any) => row.sn === item.sn)
+  tableSnData.value.splice(index, 1)
+}
+
+const callbackSnDrawer = (done: Fn) => {
+  SNDataMap.set(formSn.Id as number, tableSnData.value)
+  let arrs = tableSnData.value.map((item:any) => item.sn);
+  emit('onCount', SNDataMap.get(formSn.Id as number)?.length, formSn.Id as number,arrs)
+  done()
+  nextTick(() => {
+    resetSnForm(ruleSnFormRef.value)
+    tableSnData.value = []
+  })
+}
+
+const emit = defineEmits<{ (event: 'onCount', value: any, id: number,mapArr:any): void }>()
+
+const resetSnForm = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  formEl.resetFields()
+}
+
+const addDeviceSn = (id: number, type: number) => {
+    console.log('1212',id,type)
+  formSn.Id = id
+  formSn.type = type
+  if (SNDataMap.has(id)) {
+    tableSnData.value = SNDataMap.get(id) as FormSnType[]
+  } else {
+    SNDataMap.set(id, [])
+  }
+  drawerSnRef.value?.openDrawer()
+}
+
+const drawer = ref(false)
+/**
+ * 导入xlsx
+ */
+
+// 组件状态变化的回调
+const uploadExcelFile = (event:any) => {
+    const files = event.target.files;  
+      if (files.length === 0) return;  
+      const file = files[0];  
+      const reader = new FileReader();  
+      reader.onload = (e:any) => {  
+        const data = new Uint8Array(e.target.result);  
+        const workbook = XLSX.read(data, { type: 'array' });  
+        // 假设我们知道第一个工作表包含我们需要的数据  
+        const firstSheetName = workbook.SheetNames[0];  
+        const worksheet:any = workbook.Sheets[firstSheetName];  
+        // 假设我们知道 t_sn 是第一列  
+        const tSnColumn = 'A'; // 或者使用 XLSX.utils.decode_col(columnNumber) 来从列号获取列名  
+        const columnNumber = XLSX.utils.decode_col(tSnColumn); // 但实际上我们可能直接知道列号,如 0  
+        // 使用 sheet_to_json 但只选择我们需要的列  
+        const json = XLSX.utils.sheet_to_json(worksheet, { header: 1, range: XLSX.utils.decode_range(worksheet['!ref']) });
+        // 如果表头不是第一行,或者你不想要表头,可以调整 header 选项  
+        // 提取 t_sn 列的数据(假设表头在第一行,且 t_sn 是第一列)  
+        let tSnData:any = json.map((row:any) => row[0]); // 假设 t_sn 是第一列,所以使用 row[0]  
+        let snArray = tSnData.map((sn:any) => ({ sn: sn }));
+        snArray = snArray.slice(1)
+        let arrs = [...tableSnData.value,...snArray]
+        // 去重
+        let seen = new Set();  
+        tableSnData.value = arrs.filter((item:any) => {  
+            return seen.has(item.sn) ? false : seen.add(item.sn) || true;  
+        }); 
+
+        console.log('打印',tableSnData.value)
+        // 注意:如果 t_sn 列不是第一列,你需要调整索引号  
+        // 例如,如果 t_sn 是第三列,则使用 row[2]  
+      };  
+  
+      reader.readAsArrayBuffer(file);
+
+};
+const getDeviceSn = () => SNDataMap
+const clearDeviceSn = () => SNDataMap.clear()
+const deleteDeviceSn = (id: number) => SNDataMap.delete(id)
+defineExpose({
+  getDeviceSn,
+  addDeviceSn,
+  clearDeviceSn,
+  deleteDeviceSn,tableSnData,drawerSnRef
+})
+</script>
+
+<template>
+  <Drawer ref="drawerSnRef" :handleClose="callbackSnDrawer" size="50%">
+    <el-card class="box-card" shadow="never">
+      <template #header>
+        <div class="sn-header">
+          <el-form ref="ruleSnFormRef" :model="formSn" :rules="rulesSn">
+            <el-form-item label="SN:" label-width="120px" prop="sn">
+              <el-input v-model="formSn.sn" type="text" placeholder="请输入SN" @keyup.enter="addSn(ruleSnFormRef)"   class="w-50" />
+            </el-form-item>
+          </el-form>
+          <el-button type="primary" @click="addSn(ruleSnFormRef)">添加</el-button>
+          <el-button type="success" @click="drawer = true">导入xlsx</el-button>
+        </div>
+      </template>
+      <el-table
+        ref="snTable"
+        :data="tableSnData"
+        style="width: 100%; height: 99%"
+        :header-cell-style="{
+          background: '#dedfe0',
+          height: '50px'
+        }"
+      >
+        <template v-for="item in snColumns" :key="item">
+          <el-table-column v-if="item.type === 'index'" v-bind="item" />
+          <el-table-column show-overflow-tooltip v-if="item.prop" align="center" v-bind="item">
+            <template #default="{ row }">
+              <el-button
+                v-if="item.prop === 'operation'"
+                link
+                type="danger"
+                size="small"
+                :icon="Delete"
+                @click="deleteSn(row)"
+                >删除</el-button
+              >
+            </template>
+          </el-table-column>
+        </template>
+      </el-table>
+    </el-card>
+    <el-drawer v-model="drawer" title="导入xlsx" size="50%" :destroy-on-close="true">
+        <input type="file" @change="uploadExcelFile" accept=".xlsx, .xls" /> 
+    </el-drawer>
+  </Drawer>
+
+</template>
+
+<style scoped lang="scss">
+.box-card {
+  height: 100%;
+  :deep(.el-card__body) {
+    height: calc(100% - 70px);
+  }
+  .sn-header {
+    display: flex;
+    justify-content: end;
+  }
+}
+</style>

+ 272 - 0
src/views/storehouse/outStock/modules/InStorageProduct.vue

@@ -0,0 +1,272 @@
+<script setup lang="ts">
+import {
+  Storehouse_Product_List,
+  Storehouse_ProductClass_List,
+  Storehouse_Product_Model_List,
+  Storehouse_Product_Name_List
+} from '@/api/storehouse/index'
+import { ElMessage } from 'element-plus'
+import { GlobalStore } from '@/stores/index'
+import Drawer from '@/components/Drawer/index.vue'
+import { ref, reactive, onMounted, nextTick } from 'vue'
+import { default as vElTableInfiniteScroll } from 'el-table-infinite-scroll'
+import ImageCom from '@/components/Image/index.vue'
+
+let total = 0
+const autoSelect = ref('')
+const selectTable = ref()
+const loading = ref(false)
+const globalStore = GlobalStore()
+const NameOptions = ref<any[]>([])
+const classOptions = ref<any[]>([])
+const modelOptions = ref<any[]>([])
+const tableProductData = ref<any[]>([])
+const drawerProductRef = ref<InstanceType<typeof Drawer> | null>(null)
+
+const initParam = reactive({
+  User_tokey: globalStore.GET_User_tokey,
+  T_name: '',
+  T_model: '',
+  T_class: '',
+  page: 1,
+  page_z: 20
+})
+const callbackProductDrawer = (done: () => void) => done()
+const querySearchAsync = async (queryString: string) => {
+  if (queryString) {
+    loading.value = true
+    globalStore.SET_isloading(true)
+    const results = await getNameAsync(queryString)
+    NameOptions.value = results
+    globalStore.SET_isloading(false)
+    loading.value = false
+  }
+}
+
+const getProductModelList = async () => {
+  globalStore.SET_isloading(true)
+  const res: any = await Storehouse_Product_Model_List({ T_name: autoSelect.value })
+  modelOptions.value = res.Data.map((item: any, index: number) => {
+    return {
+      value: item,
+      index: index
+    }
+  })
+  globalStore.SET_isloading(false)
+}
+const handleSelect = (item: any) => {
+  initParam.T_name = item
+  getProductModelList()
+}
+// 搜索模型
+const searchModelHandle = () => {
+  total = 0
+  initParam.page = 1
+  tableProductData.value = []
+  getProductList()
+}
+
+// 保存选中的数据id,row-key就是要指定一个key标识这一行的数据
+const getRowKey = (row: any) => {
+  return row.Id
+}
+
+// 加载第二个抽屉数据
+const load = () => {
+  if (initParam.page && total === tableProductData.value.length) {
+    ElMessage.warning('没有更多数据了!!')
+    return
+  }
+  initParam.page++
+  getProductList()
+}
+// 勾选产品
+const ProductselectionChange = (selection: any[], row: any) => emit('ontableData', row)
+const ProductSelectionAllChange = (selection: any[]) => emit('ontableDataAll', selection)
+
+const getNameAsync = async (str: string): Promise<any> => {
+  const res: any = await Storehouse_Product_Name_List({ T_name: str, T_class: initParam.T_class })
+  if (!res.Data) return
+  return res.Data.map((item: any, index: number) => {
+    return {
+      value: item,
+      index: index
+    }
+  })
+}
+// 获取产品的列表
+const getProductList = async () => {
+  const res: any = await Storehouse_Product_List({ ...initParam, T_name: autoSelect.value })
+  tableProductData.value.push(...res.Data.Data)
+  total = res.Data.Num
+  if (selectProductData.value?.length) {
+    // 设置产品的选中
+    tableProductData.value.forEach((row: any) => {
+      const matchedIndex = selectProductData.value?.findIndex((item: any) => item.Id == row.Id)
+      selectTable.value?.toggleRowSelection(row, matchedIndex != -1)
+    })
+  }
+}
+
+// 获取产品分类
+const getProductClassList = async () => {
+  const res: any = await Storehouse_ProductClass_List({ page: 1, page_z: 999 })
+  classOptions.value = res.Data.Data
+}
+
+onMounted(() => {
+  getProductList()
+  !classOptions.value.length && getProductClassList()
+})
+
+const productColumns = [
+  { type: 'selection', width: 80 },
+  { prop: 'T_img', label: '产品图片', name: 'T_img' },
+  { prop: 'T_name', label: '产品名称' },
+  { prop: 'T_class_name', label: '产品分类' },
+  { prop: 'T_model', label: '产品型号', ellipsis: true },
+  { prop: 'T_spec', label: '产品规格' },
+  { prop: 'T_relation_sn', label: '关联SN', name: 'T_relation_sn' },
+  { prop: 'T_remark', label: '备注', ellipsis: true }
+]
+
+const openDrawer = () => drawerProductRef.value?.openDrawer()
+const clearSelection = () => {
+  initParam.page = 1
+  tableProductData.value = []
+  selectTable.value?.clearSelection()
+}
+const selectTableChange = (row: any) => {
+  nextTick(() => {
+    selectTable.value?.toggleRowSelection(row, false)
+  })
+}
+
+// props
+const props = defineProps<{ selectProductData?: any[] }>()
+const selectProductData = ref(props.selectProductData)
+const emit = defineEmits<{ (event: 'ontableData', value: any): void; (event: 'ontableDataAll', value: any[]): void }>()
+
+defineExpose({
+  openDrawer,
+  clearSelection,
+  selectTableChange
+})
+</script>
+
+<template>
+  <Drawer ref="drawerProductRef" :handleClose="callbackProductDrawer" size="70%">
+    <template #header="{ params }">
+      <h4 :id="params.titleId" :class="params.titleClass">选择产品</h4>
+    </template>
+    <el-card class="box-card" shadow="never">
+      <template #header>
+        <div class="input-suffix">
+          <el-row :gutter="20" style="margin-bottom: 0">
+            <el-col :xl="5" :lg="8" :md="10" class="d-flex">
+              <span class="inline-flex items-center">产品分类:</span>
+              <el-select v-model="initParam.T_class" clearable placeholder="请选择分类~">
+                <el-option v-for="item in classOptions" :key="item.Id" :label="item.T_name" :value="item.Id" />
+              </el-select>
+            </el-col>
+            <el-col :xl="7" :lg="8" :md="10" class="d-flex">
+              <span class="inline-flex items-center">产品名称:</span>
+              <el-select
+                v-model="autoSelect"
+                filterable
+                clearable
+                remote
+                reserve-keyword
+                placeholder="按产品名称搜索"
+                remote-show-suffix
+                :remote-method="querySearchAsync"
+                :loading="loading"
+                @change="handleSelect"
+              >
+                <el-option v-for="item in NameOptions" :key="item.value" :label="item.value" :value="item.value" />
+              </el-select>
+            </el-col>
+            <el-col :xl="7" :lg="8" :md="12" class="d-flex">
+              <span class="inline-flex items-center">产品型号:</span>
+              <el-select v-model="initParam.T_model" clearable placeholder="请选择型号~">
+                <el-option v-for="item in modelOptions" :key="item.index" :label="item.value" :value="item.value" />
+              </el-select>
+              <el-button type="primary" @click="searchModelHandle">搜索</el-button>
+            </el-col>
+          </el-row>
+        </div>
+      </template>
+      <el-table
+        ref="selectTable"
+        :row-key="getRowKey"
+        :data="tableProductData"
+        style="width: 100%; height: 99%"
+        :header-cell-style="{
+          background: '#dedfe0',
+          height: '50px'
+        }"
+        v-el-table-infinite-scroll="load"
+        :infinite-scroll-immediate="false"
+        infinite-scroll-distance="'50px'"
+        @select="ProductselectionChange"
+        @select-all="ProductSelectionAllChange"
+      >
+        <template v-for="item in productColumns" :key="item">
+          <el-table-column v-if="item.type === 'index' || item.type === 'selection'" align="center" v-bind="item" />
+          <el-table-column show-overflow-tooltip v-if="!item.ellipsis && item.prop" v-bind="item">
+            <template #default="{ row }">
+              <span v-if="item.prop === 'T_relation_sn'">
+                <el-tag v-if="row.T_relation_sn === 1" effect="dark">是</el-tag>
+                <el-tag v-else type="success" effect="dark">否</el-tag>
+              </span>
+              <ImageCom v-if="item.prop === 'T_img'" :src="row.T_img" />
+            </template>
+          </el-table-column>
+          <el-table-column show-overflow-tooltip v-if="item.ellipsis && item.prop === 'T_model'" align="center" v-bind="item">
+            <template #default="{ row }">
+              <el-tooltip effect="dark" :content="row.T_model" placement="bottom">
+                {{ row.T_model }}
+              </el-tooltip>
+            </template>
+          </el-table-column>
+          <el-table-column show-overflow-tooltip v-if="item.ellipsis && item.prop === 'T_remark'" align="center" v-bind="item">
+            <template #default="{ row }">
+              <el-tooltip effect="customized" placement="left">
+                <template #content>
+                  <div class="tooltip-content">{{ row.T_remark }}</div>
+                </template>
+                {{ row.T_remark }}
+              </el-tooltip>
+            </template>
+          </el-table-column>
+        </template>
+      </el-table>
+    </el-card>
+  </Drawer>
+</template>
+
+<style scoped lang="scss">
+.tooltip-content {
+  max-width: 500px;
+  overflow-y: auto;
+}
+.box-card {
+  height: 100%;
+  :deep(.el-card__body) {
+    height: calc(100% - 70px);
+  }
+  .sn-header {
+    display: flex;
+    justify-content: end;
+  }
+  .input-suffix {
+    width: 100%;
+    .inline-flex {
+      white-space: nowrap;
+    }
+    .d-flex {
+      display: flex;
+    }
+  }
+}
+</style>