浏览代码

feat: ✨ 完成出库管理、领料出库

@sun-chaoqun 2 年之前
父节点
当前提交
490a630241

+ 1 - 1
src/layouts/Header/index.vue

@@ -9,7 +9,7 @@ import 'element-plus/theme-chalk/src/dropdown.scss'
 const globalStore = GlobalStore()
 const router = useRouter()
 const logOut = () => {
-  localStorage.clear()
+  sessionStorage.clear()
   globalStore.SET_User_Tokey('')
   globalStore.SET_User_Info({})
   globalStore.SET_Flat_Menu([])

+ 1 - 1
src/layouts/Menu/Logo.vue

@@ -11,7 +11,7 @@
 <style scoped lang="scss">
 .logo {
   width: 100%;
-  padding: 5px 15px 5px 20px;
+  padding: 15px 15px 5px 20px;
   transition: width 0.2s;
   img {
     width: 100%;

+ 24 - 0
src/router/modules/staticRouter.ts

@@ -115,6 +115,30 @@ export const staticRouter: RouteRecordRaw[] = [
             meta: {
               title: '入库详情'
             }
+          },
+          {
+            path: '/outStock',
+            name: 'OutStock',
+            component: () => import('@/views/storehouse/outStock/OutStock.vue'),
+            meta: {
+              title: '出库管理'
+            }
+          },
+          {
+            path: '/receiveOutStock',
+            name: 'ReceiveOutStock',
+            component: () => import('@/views/storehouse/outStock/ReceiveOutStock.vue'),
+            meta: {
+              title: '领料出库'
+            }
+          },
+          {
+            path: '/saleOutStock',
+            name: 'SaleOutStock',
+            component: () => import('@/views/storehouse/outStock/SaleOutStock.vue'),
+            meta: {
+              title: '销售出库'
+            }
           }
         ]
       },

+ 6 - 1
src/stores/index.ts

@@ -1,5 +1,5 @@
 import { defineStore, createPinia } from 'pinia'
-import { GlobalState } from './interface/index'
+import { GlobalState, DepotType } from './interface/index'
 import { Menu_User_List } from '@/api/role/index'
 import { User_Info } from '@/api/user/index'
 import { isEmptyObject } from '@/utils/common'
@@ -12,6 +12,7 @@ export const GlobalStore = defineStore({
     isloading: false,
     isCollapse: false,
     breadcrumb: [],
+    depotList: [], //仓库列表
     userInfo: JSON.parse(sessionStorage.getItem('User_info') as any) || {},
     UserDeptList: [],
     UserPostList: [],
@@ -28,6 +29,7 @@ export const GlobalStore = defineStore({
     GET_Dept_List: state => state.UserDeptList,
     GET_isCollapse: state => state.isCollapse,
     GET_Breadcrumb: state => state.breadcrumb,
+    GET_depotList: state => state.depotList,
     GET_User_Info: state => state.userInfo,
     GET_Menu_List: state => state.MenuList,
     GET_Flat_Menu: state => state.flatMenu
@@ -46,6 +48,9 @@ export const GlobalStore = defineStore({
     SET_isCollapse() {
       this.isCollapse = !this.isCollapse
     },
+    SET_depotList(list: DepotType[]) {
+      this.depotList = [...list]
+    },
     SET_Breadcrumb(payload: string[]) {
       let map = payload.map(item => {
         return item.replace('/', '')

+ 6 - 0
src/stores/interface/index.ts

@@ -2,6 +2,7 @@
 export interface GlobalState {
   isloading: boolean
   breadcrumb: string[]
+  depotList: DepotType[]
   isCollapse: boolean
   User_tokey: string
   userInfo?: any
@@ -15,6 +16,11 @@ export interface GlobalState {
   flatMenu: any
 }
 
+export interface DepotType {
+  T_name: string
+  Id: number
+}
+
 // export interface MenuType {
 //   Children?: MenuType[]
 //   Id: number

+ 22 - 22
src/utils/common.ts

@@ -37,31 +37,31 @@ export const getAllBreadcrumbList = (menuList: any, result: { [key: string]: any
   return result
 }
 
-const path = {
-  home: ['/home'],
-  roles: ['/roles'],
-  users: ['/users'],
-  salary: ['/salary'],
-  salarys: ['/salaryCount', '/salaryMy'],
-  workAttendance: ['/leave', '/myLeave', '/myOvertime', '/overtime', '/recordsFinance'],
-  records: ['/records']
-}
+// const path = {
+//   home: ['/home'],
+//   roles: ['/roles'],
+//   users: ['/users'],
+//   salary: ['/salary'],
+//   salarys: ['/salaryCount', '/salaryMy'],
+//   workAttendance: ['/leave', '/myLeave', '/myOvertime', '/overtime', '/recordsFinance'],
+//   records: ['/records']
+// }
 
 const permissionPath = (permission: string): string => {
-  if (path.roles.includes(permission)) {
-    return '/account/roles'
-  } else if (path.users.includes(permission)) {
-    return '/account/users'
-  } else if (path.salarys.includes(permission)) {
-    return '/salary'
-  } else if (path.salary.includes(permission)) {
-    return '/salary/salary'
-  } else if (path.workAttendance.includes(permission)) {
-    return '/workAttendance'
-  } else if (path.records.includes(permission)) {
-    return '/workAttendance/records'
+  switch (permission) {
+    case '/roles':
+      return '/account/roles'
+    case '/users':
+      return '/account/users'
+    case '/salarys':
+      return '/salarys'
+    case '/workAttendance':
+      return '/workAttendance'
+    case '/records':
+      return '/workAttendance/records'
+    default:
+      return '/home'
   }
-  return '/home'
 }
 
 const get_name = (name: string): string => {

+ 3 - 0
src/views/Login.vue

@@ -74,6 +74,9 @@ 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>

+ 169 - 0
src/views/home/Home.vue

@@ -4,6 +4,20 @@
   <div class="home">
     <!-- <img class="img" src="@/assets/images/1.jpg" /> -->
     <h1>welcome</h1>
+    <div class="wrap">
+      <ul>
+        <li></li>
+        <li></li>
+        <li></li>
+        <li></li>
+        <li></li>
+        <li></li>
+        <li></li>
+        <li></li>
+        <li></li>
+        <li></li>
+      </ul>
+    </div>
   </div>
 </template>
 
@@ -16,6 +30,161 @@
   align-items: center;
   h1 {
     font-size: 4rem;
+    position: absolute;
+  }
+  .wrap {
+    width: 100%;
+    height: 100%;
+    padding: 40px 0;
+    // position: fixed;
+    position: relative;
+    // top: 50%;
+    opacity: 0.8;
+    // background: linear-gradient(to bottom right, #50a3a2, #53e3a6);
+    // background: -webkit-linear-gradient(to bottom right, #50a3a2, #53e3a6);
+  }
+  .wrap ul {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    z-index: -10;
+  }
+  .wrap ul li {
+    list-style-type: none;
+    display: block;
+    position: absolute;
+    bottom: -120px;
+    width: 15px;
+    height: 15px;
+    z-index: -8;
+    // background-color: rgba(255, 255, 255, 0.15);
+    background: linear-gradient(to bottom right, #50a3a2, #53e3a6);
+    background: -webkit-linear-gradient(to bottom right, #50a3a2, #53e3a6);
+    animotion: square 25s infinite;
+    -webkit-animation: square 25s infinite;
+  }
+  .wrap ul li:nth-child(1) {
+    left: 0;
+    animation-duration: 10s;
+    -moz-animation-duration: 10s;
+    -o-animation-duration: 10s;
+    -webkit-animation-duration: 10s;
+  }
+  .wrap ul li:nth-child(2) {
+    width: 40px;
+    height: 40px;
+    left: 10%;
+    animation-duration: 15s;
+    -moz-animation-duration: 15s;
+    -o-animation-duration: 15s;
+    -webkit-animation-duration: 15s;
+  }
+  .wrap ul li:nth-child(3) {
+    left: 20%;
+    width: 25px;
+    height: 25px;
+    animation-duration: 12s;
+    -moz-animation-duration: 12s;
+    -o-animation-duration: 12s;
+    -webkit-animation-duration: 12s;
+  }
+  .wrap ul li:nth-child(4) {
+    width: 50px;
+    height: 50px;
+    left: 30%;
+    -webkit-animation-delay: 3s;
+    -moz-animation-delay: 3s;
+    -o-animation-delay: 3s;
+    animation-delay: 3s;
+    animation-duration: 12s;
+    -moz-animation-duration: 12s;
+    -o-animation-duration: 12s;
+    -webkit-animation-duration: 12s;
+  }
+  .wrap ul li:nth-child(5) {
+    width: 60px;
+    height: 60px;
+    left: 40%;
+    animation-duration: 10s;
+    -moz-animation-duration: 10s;
+    -o-animation-duration: 10s;
+    -webkit-animation-duration: 10s;
+  }
+  .wrap ul li:nth-child(6) {
+    width: 75px;
+    height: 75px;
+    left: 50%;
+    -webkit-animation-delay: 7s;
+    -moz-animation-delay: 7s;
+    -o-animation-delay: 7s;
+    animation-delay: 7s;
+  }
+  .wrap ul li:nth-child(7) {
+    left: 60%;
+    animation-duration: 8s;
+    -moz-animation-duration: 8s;
+    -o-animation-duration: 8s;
+    -webkit-animation-duration: 8s;
+  }
+  .wrap ul li:nth-child(8) {
+    width: 90px;
+    height: 90px;
+    left: 70%;
+    -webkit-animation-delay: 4s;
+    -moz-animation-delay: 4s;
+    -o-animation-delay: 4s;
+    animation-delay: 4s;
+  }
+  .wrap ul li:nth-child(9) {
+    width: 100px;
+    height: 100px;
+    left: 80%;
+    animation-duration: 20s;
+    -moz-animation-duration: 20s;
+    -o-animation-duration: 20s;
+    -webkit-animation-duration: 20s;
+  }
+  .wrap ul li:nth-child(10) {
+    width: 120px;
+    height: 120px;
+    left: 90%;
+    -webkit-animation-delay: 6s;
+    -moz-animation-delay: 6s;
+    -o-animation-delay: 6s;
+    animation-delay: 6s;
+    animation-duration: 30s;
+    -moz-animation-duration: 30s;
+    -o-animation-duration: 30s;
+    -webkit-animation-duration: 30s;
+  }
+
+  @keyframes square {
+    0% {
+      -webkit-transform: translateY(0);
+      transform: translateY(0);
+    }
+    100% {
+      bottom: 400px;
+      transform: rotate(600deg);
+      -webit-transform: rotate(600deg);
+      -webkit-transform: translateY(-800);
+      transform: translateY(-800);
+    }
+  }
+  @-webkit-keyframes square {
+    0% {
+      -webkit-transform: translateY(0);
+      transform: translateY(0);
+    }
+    100% {
+      bottom: 400px;
+      transform: rotate(600deg);
+      -webit-transform: rotate(600deg);
+      -webkit-transform: translateY(-800);
+      transform: translateY(-800);
+    }
   }
 }
 .img {

+ 11 - 2
src/views/login.scss

@@ -13,7 +13,7 @@
     // background-image: linear-gradient(0deg, #402565, #30be96);
     width: 26.25rem;
     padding: 1.5rem;
-    padding-top: 35px;
+    // padding-top: 35px;
     border-radius: 6px;
     :deep(.el-form-item__label) {
       font-weight: 600;
@@ -25,12 +25,21 @@
     .view {
       cursor: pointer;
     }
+    .logo {
+      height: 5.125rem;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      img {
+        height: 100%;
+      }
+    }
   }
   .title {
     display: flex;
     flex-direction: column;
     align-items: center;
-    margin: 30px 30px 10px;
+    margin: 0px 30px 10px;
   }
   h2 {
     color: #2684b4;

+ 5 - 3
src/views/storehouse/inventory/InStorage.vue

@@ -52,8 +52,8 @@ const initParam = reactive({
 })
 
 const searchHandle = () => {
-  T_date.value && (initParam.T_end_date = T_date.value[1])
-  T_date.value && (initParam.T_start_date = T_date.value[0])
+  initParam.T_end_date = T_date.value ? T_date.value[1] : ''
+  initParam.T_start_date = T_date.value ? T_date.value[0] : ''
   TableRef.value?.searchTable()
 }
 /**
@@ -66,10 +66,12 @@ interface ItemType {
   T_name: string
   Id: number
 }
-const options = ref<ItemType[]>()
+const options = ref<ItemType[]>([])
 const getDepotList = async () => {
+  if (globalStore.GET_depotList.length) 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)
 }
 onMounted(() => {
   getDepotList()

+ 12 - 2
src/views/storehouse/inventory/InStorageForm.vue

@@ -176,6 +176,12 @@ const ProductselectionChange = (row: any) => {
     tableData.value.splice(index, 1)
   }
 }
+/**
+ * 全选
+ */
+const ProductSelectionAllChange = (selection: any[]) => {
+  tableData.value = selection
+}
 
 /**
  * 添加sn 号
@@ -305,7 +311,7 @@ defineExpose({
             </template>
           </el-table>
         </el-form-item>
-        <el-form-item label="入库仓库:" :label-width="formLabelWidth" prop="T_remark">
+        <el-form-item label="备注:" :label-width="formLabelWidth" prop="T_remark">
           <el-input
             v-model="form.T_remark"
             :autosize="{ minRows: 4, maxRows: 6 }"
@@ -319,8 +325,12 @@ defineExpose({
             <el-button color="#626aef" @click="AddInStorage(ruleFormRef)">提交</el-button>
           </el-divider>
         </div>
-        <InStorageProduct ref="drawerProductRef" @ontableData="ProductselectionChange" />
         <InStorageSn ref="drawerSnRef" @onCount="autoGetCount" />
+        <InStorageProduct
+          ref="drawerProductRef"
+          @ontableData="ProductselectionChange"
+          @ontableDataAll="ProductSelectionAllChange"
+        />
       </el-form>
     </Drawer>
   </div>

+ 3 - 1
src/views/storehouse/inventory/InStorageProduct.vue

@@ -81,6 +81,7 @@ const load = () => {
 }
 // 勾选产品
 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 })
@@ -127,7 +128,7 @@ const selectTableChange = (row: any) => {
     selectTable.value?.toggleRowSelection(row, false)
   })
 }
-const emit = defineEmits<{ (event: 'ontableData', value: any): void }>()
+const emit = defineEmits<{ (event: 'ontableData', value: any): void; (event: 'ontableDataAll', value: any[]): void }>()
 
 defineExpose({
   openDrawer,
@@ -184,6 +185,7 @@ defineExpose({
         }"
         v-el-table-infinite-scroll="load"
         @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" />

+ 244 - 3
src/views/storehouse/outStock/OutStock.vue

@@ -1,6 +1,247 @@
-<script setup lang="ts"></script>
+<script setup lang="ts">
+import { ElMessage } from 'element-plus'
+import { useRouter } from 'vue-router'
+import { GlobalStore } from '@/stores/index'
+import { View, Van } 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, onMounted, nextTick } from 'vue'
+import TableBase from '@/components/TableBase/index.vue'
+import type { ColumnProps } from '@/components/TableBase/interface/index'
+import { Storehouse_StockOut_List, Storehouse_Depot_List, Storehouse_StockOut_Edit } from '@/api/storehouse/index'
+
+type Fn = () => void
+
+const router = useRouter()
+const formLabelWidth = ref('120px')
+const globalStore = GlobalStore()
+const ruleFormRef = ref<FormInstance>()
+const TableRef = ref<InstanceType<typeof TableBase> | null>(null)
+const DrawerRef = ref<InstanceType<typeof Drawer> | null>(null)
+
+const delivery_type = [
+  { name: '自送', id: 1 },
+  { name: '自提', id: 2 },
+  { name: '快递', id: 3 }
+]
+const columns: ColumnProps[] = [
+  { type: 'index', label: '序号', width: 80 },
+  { prop: 'T_number', label: '出库单号' },
+  { prop: 'T_receive_name', label: '领取人' },
+  { prop: 'T_depot_name', label: '出库仓库' },
+  { prop: 'T_date', label: '出库日期' },
+  { prop: 'T_type', label: '出库类型', name: 'T_type' },
+  { prop: 'T_contract_number', label: '合同编号' },
+  { prop: 'operation', label: '操作', width: 170, fixed: 'right', align: 'left ' }
+]
+
+const form = reactive({
+  T_number: '',
+  T_remark: '',
+  T_delivery_type: '',
+  T_signer_unit: '',
+  T_signer: '',
+  T_signer_phone: '',
+  T_signer_date: '',
+  T_courier_number: ''
+})
+
+const rules = reactive<FormRules>({
+  T_delivery_type: [{ required: true, message: '请选择送货方式', trigger: 'blur' }],
+  T_signer_unit: [{ required: true, message: '请输入签收单位', trigger: 'blur' }],
+  T_signer: [{ required: true, message: '请输入签收人', trigger: 'blur' }],
+  T_signer_phone: [{ required: true, validator: validate_T_phone, trigger: 'blur' }],
+  T_signer_date: [{ required: true, message: '请选择签收日期', trigger: 'blur' }]
+})
+
+/**
+ * 查看详情
+ */
+const preview = (id: string) => {
+  router.push({ name: 'InStorageDetail', params: { id } })
+}
+
+// 搜索
+const T_date = ref<string[]>([])
+const initParam = reactive({
+  User_tokey: globalStore.GET_User_tokey,
+  T_end_date: '',
+  T_start_date: '',
+  T_depot_id: ''
+})
+const searchHandle = () => {
+  initParam.T_end_date = T_date.value ? T_date.value[1] : ''
+  initParam.T_start_date = T_date.value ? T_date.value[0] : ''
+  TableRef.value?.searchTable()
+}
+
+// 发货
+const deliveryEdit = (number: string) => {
+  form.T_number = number
+  DrawerRef.value?.openDrawer()
+}
+const callbackDrawer = (done: Fn) => done()
+const closeDrawer = () => {
+  resetForm(ruleFormRef.value)
+  DrawerRef.value?.closeDrawer()
+}
+const addOutStock = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  formEl.validate(async valid => {
+    if (valid) {
+      const res: any = await Storehouse_StockOut_Edit({ User_tokey: globalStore.GET_User_tokey, ...form })
+      if (res.Code === 200) {
+        ElMessage.success('发货成功!')
+        nextTick(() => {
+          closeDrawer()
+          // TableRef.value?.searchTable()
+        })
+      }
+    }
+  })
+}
+/**
+ * 重置表单
+ */
+const resetForm = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  formEl.resetFields()
+}
+// 拿到仓库列表
+interface ItemType {
+  T_name: string
+  Id: number
+}
+const options = ref<ItemType[]>([])
+const getDepotList = async () => {
+  if (globalStore.GET_depotList.length) 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)
+}
+onMounted(() => {
+  getDepotList()
+})
+</script>
 
 <template>
-  <div class="out-stock"></div>
+  <div class="out-stock">
+    <TableBase ref="TableRef" :columns="columns" :requestApi="Storehouse_StockOut_List" :initParam="initParam">
+      <template #table-header>
+        <div class="input-suffix">
+          <el-row :gutter="20" style="margin-bottom: 0">
+            <el-col :xl="6" :lg="9" :md="11" style="display: flex">
+              <span class="inline-flex items-center">出库日期:</span>
+              <el-date-picker
+                v-model="T_date"
+                type="daterange"
+                range-separator="~"
+                start-placeholder="开始时间"
+                end-placeholder="结束时间"
+                format="YYYY-MM-DD"
+                value-format="YYYY-MM-DD"
+              />
+            </el-col>
+            <el-col :xl="6" :lg="7" :md="9" style="display: flex">
+              <span class="inline-flex items-center">仓库:</span>
+              <el-select v-model="initParam.T_depot_id" clearable placeholder="请选择仓库~">
+                <el-option v-for="item in options" :key="item.Id" :label="item.T_name" :value="item.Id" />
+              </el-select>
+              <el-button type="primary" @click="searchHandle">搜索</el-button>
+            </el-col>
+            <el-col :xl="12" :lg="8" :md="4" class="btn"
+              ><el-button type="primary" @click="router.push('/receiveOutStock')">领料出库</el-button>
+              <el-button type="primary" @click="router.push('/saleOutStock')">销售出库</el-button>
+            </el-col>
+          </el-row>
+        </div>
+      </template>
+      <template #T_type="{ row }">
+        <el-tag v-if="row.T_type === 1" type="success"> 领料出库 </el-tag>
+        <el-tag v-else> 销售出库 </el-tag>
+      </template>
+      <template #right="{ row }">
+        <el-button link type="success" size="small" :icon="View" @click="preview(row.T_number)">详情</el-button>
+        <el-button
+          v-if="row.T_type !== 1"
+          link
+          type="primary"
+          size="small"
+          :icon="Van"
+          @click="deliveryEdit(row.T_number)"
+          >发货管理</el-button
+        >
+      </template>
+    </TableBase>
+    <Drawer ref="DrawerRef" :handleClose="callbackDrawer">
+      <template #header="{ params }">
+        <h4 :id="params.titleId" :class="params.titleClass">发货管理</h4>
+      </template>
+      <el-form ref="ruleFormRef" :model="form" :rules="rules">
+        <el-divider border-style="dashed" />
+        <el-form-item label="送货方式:" :label-width="formLabelWidth" prop="T_delivery_type">
+          <el-select v-model="form.T_delivery_type" clearable placeholder="请选择送货方式~">
+            <el-option v-for="item in delivery_type" :key="item.id" :label="item.name" :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="签收单位:" :label-width="formLabelWidth" prop="T_signer_unit">
+          <el-input v-model="form.T_signer_unit" type="text" placeholder="请输入签收单位" />
+        </el-form-item>
+        <el-form-item label="签收人:" :label-width="formLabelWidth" prop="T_signer">
+          <el-input v-model="form.T_signer" type="text" placeholder="请输入签收人" />
+        </el-form-item>
+        <el-form-item label="签收人电话:" :label-width="formLabelWidth" prop="T_signer_phone">
+          <el-input v-model="form.T_signer_phone" type="text" placeholder="请输入签收人电话" />
+        </el-form-item>
+        <el-form-item label="签收日期:" :label-width="formLabelWidth" prop="T_signer_date">
+          <el-date-picker
+            class="my-date-picker"
+            v-model="form.T_signer_date"
+            type="date"
+            style="width: 100%"
+            placeholder="选择签收日期"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+          />
+        </el-form-item>
+        <el-form-item label="物流单号:" :label-width="formLabelWidth" prop="T_courier_number">
+          <el-input v-model="form.T_courier_number" type="text" placeholder="请输入物流单号" />
+        </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-button @click="closeDrawer">取消</el-button>
+          <el-button type="primary" @click="addOutStock(ruleFormRef)">提交</el-button>
+        </div>
+      </el-form>
+    </Drawer>
+  </div>
 </template>
-<style scoped></style>
+
+<style scoped lang="scss">
+.out-stock {
+  :deep(.el-drawer__header) {
+    margin-bottom: 0;
+  }
+  .input-suffix {
+    width: 100%;
+    .inline-flex {
+      white-space: nowrap;
+    }
+    .btn {
+      display: flex;
+      justify-content: end;
+      .el-button {
+        padding: 0 20px;
+      }
+    }
+  }
+}
+</style>

+ 357 - 0
src/views/storehouse/outStock/ReceiveOutStock.vue

@@ -0,0 +1,357 @@
+<script setup lang="ts">
+import { ElMessage } from 'element-plus'
+import { ref, reactive, nextTick } from 'vue'
+import { useRouter } from 'vue-router'
+import { GlobalStore } from '@/stores/index'
+import Drawer from '@/components/Drawer/index.vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { Delete, CirclePlus } from '@element-plus/icons-vue'
+import { Storehouse_StockOut_Add, Storehouse_ProductClass_List } from '@/api/storehouse/index'
+import ReceiveUser from './receiveUser.vue'
+import InStorageSn from '../inventory/InStorageSn.vue'
+import InStorageProduct from '../inventory/InStorageProduct.vue'
+
+// type Fn = () => void
+interface FormType {
+  T_type: number
+  T_uuid: string
+  T_receive: string
+  T_number: string
+  T_depot_id: any
+  T_product: any
+  T_date: string
+  T_remark: string
+  T_contract_number: string
+}
+
+const router = useRouter()
+const tableData = ref<any[]>([])
+const globalStore = GlobalStore()
+const formLabelWidth = ref('120px')
+const ruleFormRef = ref<FormInstance>()
+const receiveUserdialog = ref<InstanceType<typeof ReceiveUser> | null>(null)
+const drawerRef = ref<InstanceType<typeof Drawer> | null>(null)
+const drawerSnRef = ref<InstanceType<typeof InStorageSn> | null>(null)
+const drawerProductRef = ref<InstanceType<typeof InStorageProduct> | null>(null)
+const classOptions = ref<any[]>([])
+
+const form = reactive<FormType>({
+  T_type: 1,
+  T_uuid: '',
+  T_receive: '',
+  T_number: '',
+  T_depot_id: '',
+  T_product: '',
+  T_date: '',
+  T_remark: '',
+  T_contract_number: ''
+})
+
+const validate_T_product = (rule: any, value: any, callback: any) => {
+  if (value.includes(undefined) || value === '') {
+    callback(new Error('请填写产品数量'))
+  } else if (value.includes(null)) {
+    callback(new Error('请添加产品SN'))
+  } else {
+    callback()
+  }
+}
+
+const rules = reactive<FormRules>({
+  T_product: [{ required: true, validator: validate_T_product, trigger: 'blur' }],
+  T_receive: [{ required: true, message: '请选择经办人', 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_model', 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 = drawerSnRef.value?.getDeviceSn()
+  const isEmpty = determineSNorCount(DeviceSnData)
+  if (isEmpty && isEmpty === 'count') return [undefined]
+  if (isEmpty && isEmpty === 'sn') return [null]
+
+  return tableData.value.map((item: any) => {
+    let product: any = ''
+    if (item.T_relation_sn === 1) {
+      DeviceSnData?.forEach((value: any, key: number) => {
+        if (item.Id === key) {
+          let str = ''
+          value.forEach(
+            (snObj: any, index: number, arr: any[]) => (str += `${snObj.sn}${index === arr.length - 1 ? '' : ','}`)
+          )
+          product = `${item.Id}-${item.count}-${str}`
+        }
+      })
+    } else {
+      product = `${item.Id}-${item.count}-`
+    }
+    return product + '|'
+  })
+}
+/**
+ * 判断sn or 数量是否为空
+ */
+const determineSNorCount = (DeviceSnData: any) => {
+  for (const item of tableData.value) {
+    if (!item.count && item.T_relation_sn !== 1) return 'count'
+    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(form)
+      const res: any = await Storehouse_StockOut_Add({
+        User_tokey: globalStore.GET_User_tokey,
+        ...form,
+        T_product: form.T_product.join(''),
+        T_receive: form.T_uuid
+      })
+      if (res.Code === 200) {
+        ElMessage.success('出库成功!')
+        drawerProductRef.value?.clearSelection()
+        nextTick(() => {
+          // emit('onUpdateList')
+          resetForm(ruleFormRef.value)
+          // drawerRef.value?.closeDrawer()
+          router.back()
+        })
+      }
+    }
+  })
+}
+
+const deleteProduct = (row: any) => {
+  tableData.value = tableData.value.filter(item => item.Id !== row.Id)
+
+  // 设置产品的选中
+  drawerProductRef.value?.selectTableChange(row)
+  // 删除设备得sn
+  drawerSnRef.value?.deleteDeviceSn(row.Id)
+}
+
+const closeReceive = () => {
+  resetForm(ruleFormRef.value)
+  router.back()
+}
+/**
+ * 重置表单
+ */
+const resetForm = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  tableData.value = []
+  formEl.resetFields()
+}
+
+/**
+ * 获取产品分类
+ */
+const getProductClassList = async () => {
+  const res: any = await Storehouse_ProductClass_List({ page: 1, page_z: 999 })
+  classOptions.value = res.Data.Data
+}
+/**
+ * 添加产品
+ */
+const AddProductionDetailed = () => {
+  !classOptions.value.length && getProductClassList()
+  drawerProductRef.value?.openDrawer()
+}
+/**
+ * 产品选择 是否
+ */
+const ProductselectionChange = (row: any) => {
+  const index = tableData.value.findIndex((item: any) => item.Id === row.Id)
+  if (index === -1) {
+    row.count = ''
+    tableData.value.push(row)
+  } else {
+    tableData.value.splice(index, 1)
+  }
+}
+/**
+ * 全选
+ */
+const ProductSelectionAllChange = (selection: any[]) => {
+  tableData.value = selection
+}
+
+/**
+ * 添加sn 号
+ */
+const addDeviceSn = (id: number) => {
+  drawerSnRef.value?.addDeviceSn(id)
+}
+/**
+ * 自动计算 count
+ */
+const autoGetCount = (length: number, id: number) => {
+  tableData.value.forEach((item: any) => {
+    if (item.Id === id) {
+      item.count = length
+    }
+  })
+}
+
+/**
+ * 选择经办人
+ */
+const selectApprover = () => receiveUserdialog.value?.openDialog()
+const getReceiveInfo = ({ T_uuid, T_name }: { T_uuid: string; T_name: string }) => {
+  form.T_receive = T_name
+  form.T_uuid = T_uuid
+}
+
+// 注册事件
+// const emit = defineEmits<{ (event: 'onUpdateList'): void }>()
+
+// 接受props
+const options = globalStore.GET_depotList
+</script>
+
+<template>
+  <div class="receive-outStock">
+    <h4 class="title">领料出库</h4>
+    <el-form ref="ruleFormRef" :model="form" :rules="rules">
+      <el-divider border-style="dashed" />
+      <el-form-item label="出库单号:" :label-width="formLabelWidth" prop="T_number">
+        <el-input v-model="form.T_number" disabled type="text" 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" clearable placeholder="请选择入库仓库~">
+          <el-option v-for="item in 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"
+        />
+      </el-form-item>
+      <el-form-item label="经办人:" :label-width="formLabelWidth" prop="T_receive">
+        <el-input
+          v-model="form.T_receive"
+          type="text"
+          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 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.Id)"
+                    >添加</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>
+                <el-image v-if="item.prop === 'T_img'" style="height: 50px" :src="row.T_img" fit="cover" />
+              </template>
+            </el-table-column>
+            <el-table-column 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="closeReceive">取消</el-button>
+          <el-button color="#626aef" @click="AddInStorage(ruleFormRef)">提交</el-button>
+        </el-divider>
+      </div>
+    </el-form>
+    <InStorageSn ref="drawerSnRef" @onCount="autoGetCount" />
+    <ReceiveUser ref="receiveUserdialog" @onUserInfo="getReceiveInfo" />
+    <InStorageProduct
+      ref="drawerProductRef"
+      @ontableData="ProductselectionChange"
+      @ontableDataAll="ProductSelectionAllChange"
+    />
+  </div>
+</template>
+<style scoped lang="scss">
+@import './index.scss';
+</style>

+ 86 - 0
src/views/storehouse/outStock/SaleOutStock.vue

@@ -0,0 +1,86 @@
+<script setup lang="ts">
+import { ref, reactive, nextTick } from 'vue'
+import { useRouter } from 'vue-router'
+import { GlobalStore } from '@/stores/index'
+import type { FormInstance, FormRules } from 'element-plus'
+import { Delete, CirclePlus } from '@element-plus/icons-vue'
+import {
+  Storehouse_StockOut_Add,
+  Storehouse_ProductClass_List,
+  Storehouse_Contract_Product_List
+} from '@/api/storehouse/index'
+import ReceiveUser from './receiveUser.vue'
+import InStorageSn from '../inventory/InStorageSn.vue'
+import InStorageProduct from '../inventory/InStorageProduct.vue'
+
+interface FormType {
+  T_type: number
+  T_uuid: string
+  T_receive: string
+  T_number: string
+  T_depot_id: any
+  T_product: any
+  T_date: string
+  T_remark: string
+  T_contract_number: string
+}
+
+const router = useRouter()
+const tableData = ref<any[]>([])
+const globalStore = GlobalStore()
+const formLabelWidth = ref('120px')
+const ruleFormRef = ref<FormInstance>()
+const form = reactive<FormType>({
+  T_type: 1,
+  T_uuid: '',
+  T_receive: '',
+  T_number: '',
+  T_depot_id: '',
+  T_product: '',
+  T_date: '',
+  T_remark: '',
+  T_contract_number: ''
+})
+
+const validate_T_product = (rule: any, value: any, callback: any) => {
+  if (value.includes(undefined) || value === '') {
+    callback(new Error('请填写产品数量'))
+  } else if (value.includes(null)) {
+    callback(new Error('请添加产品SN'))
+  } else {
+    callback()
+  }
+}
+
+const rules = reactive<FormRules>({
+  T_product: [{ required: true, validator: validate_T_product, trigger: 'blur' }],
+  T_receive: [{ required: true, message: '请选择经办人', trigger: 'blur' }],
+  T_depot_id: [{ required: true, message: '请选择仓库', trigger: 'blur' }],
+  T_date: [{ required: true, message: '请选择出库日期', trigger: 'blur' }]
+})
+
+const options = globalStore.GET_depotList
+</script>
+<template>
+  <div class="receive-outStock">
+    <h4 class="title">销售出库</h4>
+    <el-form ref="ruleFormRef" :model="form" :rules="rules">
+      <el-divider border-style="dashed" />
+      <el-form-item label="出库单号:" :label-width="formLabelWidth" prop="T_number">
+        <el-input v-model="form.T_number" disabled 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_number" type="text" 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" clearable placeholder="请选择入库仓库~">
+          <el-option v-for="item in options" :key="item.Id" :label="item.T_name" :value="item.Id" />
+        </el-select>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<style scoped lang="scss">
+@import './index.scss';
+</style>

+ 42 - 0
src/views/storehouse/outStock/index.scss

@@ -0,0 +1,42 @@
+.tooltip-content {
+  max-width: 500px;
+  overflow-y: auto;
+}
+.receive-outStock {
+  .title {
+    padding-top: 1rem;
+    font-size: 1.5rem;
+  }
+  :deep(.el-drawer__header) {
+    margin-bottom: 0;
+  }
+  :deep(.el-divider__text.is-center) {
+    background: none;
+  }
+  .box-card {
+    height: 100%;
+    :deep(.el-card__body) {
+      height: calc(100% - 70px);
+    }
+  }
+  .btn {
+    margin-top: 32px;
+    display: flex;
+    justify-content: center;
+    .el-button {
+      padding: 0 32px;
+    }
+  }
+  .w-50 {
+    width: 21.5rem;
+  }
+  .input-suffix {
+    width: 100%;
+    .inline-flex {
+      white-space: nowrap;
+    }
+    .d-flex {
+      display: flex;
+    }
+  }
+}

+ 87 - 0
src/views/storehouse/outStock/receiveUser.vue

@@ -0,0 +1,87 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+import { User_List } from '@/api/user/index'
+import Dialog from '@/components/dialog/Dialog.vue'
+import { GlobalStore } from '@/stores/index'
+import TableBase from '@/components/TableBase/index.vue'
+import { ColumnProps } from '@/components/TableBase/interface/index'
+
+const search = ref('')
+const globalStore = GlobalStore()
+const dialog = ref<InstanceType<typeof Dialog> | null>(null)
+const tableRef = ref<InstanceType<typeof TableBase> | null>(null)
+
+const Dialogcolumns: ColumnProps[] = [{ prop: 'T_name', label: '名字', name: 'T_name' }]
+const InitParam = {
+  User_tokey: globalStore.GET_User_tokey,
+  T_name: '',
+  T_dept_leader: 1
+}
+const searchHandle = () => {
+  InitParam.T_name = search.value
+  tableRef.value?.searchTable()
+}
+/**
+ * 获取信息
+ */
+const getApproverInfo = (row: any) => {
+  // uuid = row.T_uuid
+  // form.value.T_approver = row.T_name
+  emit('onUserInfo', row)
+  dialog.value?.DialogClose()
+}
+
+const emit = defineEmits<{ (event: 'onUserInfo', value: any): void }>()
+
+/**
+ * 配置高度
+ */
+const onResize = () => {
+  const height = document.documentElement.clientHeight
+  return height / 2
+}
+
+const openDialog = () => dialog.value?.DialogOpen()
+defineExpose({
+  openDialog
+})
+</script>
+
+<template>
+  <Dialog ref="dialog" width="30%">
+    <template #header>
+      <h3>选择经办人</h3>
+    </template>
+    <TableBase
+      ref="tableRef"
+      :columns="Dialogcolumns"
+      :initParam="InitParam"
+      :requestApi="User_List"
+      layout="total, prev, pager, next"
+      :onResize="onResize"
+    >
+      <template #table-header>
+        <el-row :gutter="20">
+          <el-col :span="24" class="d-flex">
+            <span class="inline-flex">账户查询:</span>
+            <el-input v-model="search" type="text" />
+            <el-button type="primary" @click="searchHandle">搜索</el-button>
+          </el-col>
+        </el-row>
+      </template>
+      <template #T_name="{ row }">
+        <el-button type="primary" link @click="getApproverInfo(row)">{{ row.T_name }}</el-button>
+      </template>
+    </TableBase>
+  </Dialog>
+</template>
+
+<style scoped>
+.inline-flex {
+  white-space: nowrap;
+}
+.d-flex {
+  display: flex;
+  align-items: center;
+}
+</style>