Commit c3f5b1f4 by qinjianhui

feat: 缺货统计功能开发

parent efec55ef
...@@ -34,7 +34,6 @@ declare module 'vue' { ...@@ -34,7 +34,6 @@ declare module 'vue' {
ElImage: typeof import('element-plus/es')['ElImage'] ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption'] ElOption: typeof import('element-plus/es')['ElOption']
...@@ -54,7 +53,6 @@ declare module 'vue' { ...@@ -54,7 +53,6 @@ declare module 'vue' {
ElTag: typeof import('element-plus/es')['ElTag'] ElTag: typeof import('element-plus/es')['ElTag']
ElTimeline: typeof import('element-plus/es')['ElTimeline'] ElTimeline: typeof import('element-plus/es')['ElTimeline']
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem'] ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree'] ElTree: typeof import('element-plus/es')['ElTree']
ElUpload: typeof import('element-plus/es')['ElUpload'] ElUpload: typeof import('element-plus/es')['ElUpload']
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host",
"build": "npm run lint && vue-tsc && vite build", "build": "npm run lint && vue-tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "vue-tsc --noEmit && eslint" "lint": "vue-tsc --noEmit && eslint"
......
...@@ -48,3 +48,14 @@ export function addExternalAuthorisationApi( ...@@ -48,3 +48,14 @@ export function addExternalAuthorisationApi(
) { ) {
return axios.post<never, BaseRespData<never>>(url, data) return axios.post<never, BaseRespData<never>>(url, data)
} }
export function saveInventoryLowerLimitApi(inventoryLowerLimit: number) {
return axios.get<never, BaseRespData<never>>(
'factory/baseExternalAccount/setInventoryWarningFloor',
{ params: { inventoryWarningFloor: inventoryLowerLimit } },
)
}
export function getInventoryLowerLimitApi() {
return axios.get<never, BaseRespData<number>>(
'factory/baseExternalAccount/getInventoryWarningFloor',
)
}
import axios from './axios'
import { BasePaginationData } from '@/types/api'
import { SearchForm, OutOfStockItem } from '@/types/api/outOfStockStatistics'
export function getOutOfStockStatisticsListApi(
data: SearchForm,
currentPage: number,
pageSize: number,
) {
return axios.post<never, BasePaginationData<OutOfStockItem>>(
'stockOutStatistics/getStockOutStatistics',
{ ...data, currentPage, pageSize },
)
}
...@@ -163,6 +163,13 @@ const router = createRouter({ ...@@ -163,6 +163,13 @@ const router = createRouter({
component: CustomersPage, component: CustomersPage,
}, },
{ {
path: '/supply/out-of-stock-statistics',
meta: {
title: '缺货统计',
},
component: () => import('@/views/supply/OutOfStockStatistics.vue')
},
{
path: '/system/delivery-note', path: '/system/delivery-note',
meta: { meta: {
title: '定制发货单', title: '定制发货单',
......
...@@ -148,7 +148,18 @@ const menu: MenuItem[] = [ ...@@ -148,7 +148,18 @@ const menu: MenuItem[] = [
}, },
], ],
}, },
{
index: '14',
id: 14,
label: '供应',
children: [
{
label:'缺货统计',
index:'/supply/out-of-stock-statistics',
id:1,
},
],
},
{ {
index: '12', index: '12',
id: 4, id: 4,
......
export interface OutOfStockItem {
id?: number
variantImage?: string
warehouseName?: string
locationCode?: string
warehouseSku?: string
productNo?: string
skuName?: string
currency?: string
costPrice?: number
outOfStockQuantity?: number
salesQuantity?: number
warehouseSpu?: string
inventory?: number
occupyInventory?: number
freezeInventory?: number
purchaseNotInQuantity?: number
longestDelayDays?: number
}
export interface SearchForm {
warehouseId?: string | number
warehouseSku?: string
productNo?: string
skuName?: string
}
\ No newline at end of file
...@@ -7,23 +7,30 @@ ...@@ -7,23 +7,30 @@
<span>系统配置</span> <span>系统配置</span>
</div> </div>
<div class="cardBox"> <div class="cardBox">
<div v-for="(item, index) in formList" style="width: 600px" :key="index"> <div class="card-box-item-container">
<el-form <div
v-if="item.type === 'RIIN'" v-for="(item, index) in formList"
class="form" :key="index"
ref="formRef" class="card-box-item"
label-width="120"
:model="item"
> >
<template v-if="item.type === 'RIIN'">
<div class="formBox"> <div class="formBox">
<el-form-item label="转至RIIN生产"> <div class="form-item">
<span>转至RIIN生产</span>
<el-switch <el-switch
v-model="item.enable" v-model="item.enable"
class="ml-2" class="ml-2"
style="--el-switch-on-color: #42b983" style="--el-switch-on-color: #42b983"
/> />
</el-form-item> </div>
<div class="formContent" v-if="item.enable"> <div v-if="item.enable" class="formContent">
<el-form
v-if="item.type === 'RIIN'"
ref="formRef"
class="form"
label-width="120"
:model="item"
>
<el-form-item <el-form-item
label="账号" label="账号"
prop="appKey" prop="appKey"
...@@ -48,6 +55,7 @@ ...@@ -48,6 +55,7 @@
clearable clearable
style="width: 100%" style="width: 100%"
/></el-form-item> /></el-form-item>
</el-form>
</div> </div>
</div> </div>
...@@ -57,7 +65,18 @@ ...@@ -57,7 +65,18 @@
@click="saveConfiguration(item, index)" @click="saveConfiguration(item, index)"
>保存配置</ElButton >保存配置</ElButton
> >
</el-form> </template>
<template v-if="item.type === 'TRACK'">
<div class="formBox">
<div class="form-item">
<span>物流轨迹跟踪</span>
<el-switch
v-model="item.enable"
class="ml-2"
style="--el-switch-on-color: #42b983"
/>
</div>
<div v-if="item.enable" class="formContent">
<el-form <el-form
v-if="item.type === 'TRACK'" v-if="item.type === 'TRACK'"
class="form" class="form"
...@@ -65,15 +84,6 @@ ...@@ -65,15 +84,6 @@
label-width="120" label-width="120"
:model="item" :model="item"
> >
<div class="formBox">
<el-form-item label="物流轨迹跟踪">
<el-switch
v-model="item.enable"
class="ml-2"
style="--el-switch-on-color: #42b983"
/>
</el-form-item>
<div class="formContent" v-if="item.enable">
<el-form-item <el-form-item
label="17Track账号" label="17Track账号"
prop="appKey" prop="appKey"
...@@ -98,6 +108,7 @@ ...@@ -98,6 +108,7 @@
clearable clearable
style="width: 100%" style="width: 100%"
/></el-form-item> /></el-form-item>
</el-form>
</div> </div>
</div> </div>
...@@ -107,7 +118,25 @@ ...@@ -107,7 +118,25 @@
@click="saveConfiguration(item, index)" @click="saveConfiguration(item, index)"
>保存配置</ElButton >保存配置</ElButton
> >
</el-form> </template>
</div>
<div class="card-box-item">
<div class="formBox">
<div class="form-item">
<span>库存预警下限:</span>
</div>
<div class="formContent">
<ElInput
v-model="inventoryLowerLimit"
clearable
placeholder="请输入库存预警下限"
/>
</div>
</div>
<ElButton class="btn" color="#42b983" @click="saveInventoryLowerLimit"
>保存配置</ElButton
>
</div>
</div> </div>
</div> </div>
<div class="logBox"> <div class="logBox">
...@@ -122,6 +151,8 @@ import { ...@@ -122,6 +151,8 @@ import {
addExternalAuthorisationApi, addExternalAuthorisationApi,
getExternalAuthorisationListApi, getExternalAuthorisationListApi,
baseExternalAccountLogsApi, baseExternalAccountLogsApi,
saveInventoryLowerLimitApi,
getInventoryLowerLimitApi,
} from '@/api/externalAuth' } from '@/api/externalAuth'
import { ExternalAuthListData } from '@/types/api/externalAuth' import { ExternalAuthListData } from '@/types/api/externalAuth'
...@@ -134,7 +165,7 @@ interface formType { ...@@ -134,7 +165,7 @@ interface formType {
} }
const formRef = ref() const formRef = ref()
const inventoryLowerLimit = ref<number | null>(null)
const logList = ref([]) const logList = ref([])
async function saveConfiguration(item: formType, index: number) { async function saveConfiguration(item: formType, index: number) {
let loading let loading
...@@ -222,16 +253,44 @@ async function handleClick() { ...@@ -222,16 +253,44 @@ async function handleClick() {
console.log(error) console.log(error)
} }
} }
const saveInventoryLowerLimit = async () => {
if (!inventoryLowerLimit.value) {
ElMessage.error('请输入库存预警下限')
return
}
try {
const res = await saveInventoryLowerLimitApi(inventoryLowerLimit.value)
if (res.code !== 200) {
return
}
ElMessage.success('保存配置成功')
getInventoryLowerLimit()
} catch (error) {
console.log(error)
}
}
const getInventoryLowerLimit = async () => {
try {
const res = await getInventoryLowerLimitApi()
if (res.code !== 200) {
return
}
inventoryLowerLimit.value = res.data
} catch (error) {
console.log(error)
}
}
onMounted(async () => { onMounted(async () => {
await getDetail() await getDetail()
getInventoryLowerLimit()
handleClick() handleClick()
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.card-header { .card-header {
margin-left: 200px; text-align: center;
font-size: 30px; font-size: 30px;
margin-bottom: 20px; margin-bottom: 20px;
} }
...@@ -240,9 +299,8 @@ onMounted(async () => { ...@@ -240,9 +299,8 @@ onMounted(async () => {
height: calc(100% - 60px); height: calc(100% - 60px);
} }
.cardBox { .cardBox {
flex: 3; flex: 1;
overflow-y: scroll; overflow-y: scroll;
height: 100%;
.el-card__footer { .el-card__footer {
border: none !important; border: none !important;
} }
...@@ -253,6 +311,9 @@ onMounted(async () => { ...@@ -253,6 +311,9 @@ onMounted(async () => {
padding: 20px; padding: 20px;
border: 1px solid #ebebeb; border: 1px solid #ebebeb;
border-radius: 8px; border-radius: 8px;
height: 220px;
display: flex;
flex-direction: column;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
...@@ -260,6 +321,8 @@ onMounted(async () => { ...@@ -260,6 +321,8 @@ onMounted(async () => {
padding: 20px; padding: 20px;
background-color: #f9f9f9; background-color: #f9f9f9;
border-radius: 5px; border-radius: 5px;
overflow: auto;
flex: 1;
.el-form-item:last-child { .el-form-item:last-child {
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
...@@ -274,8 +337,20 @@ onMounted(async () => { ...@@ -274,8 +337,20 @@ onMounted(async () => {
padding-top: 10px; padding-top: 10px;
height: 200px; height: 200px;
} }
.form { }
margin-bottom: 15px; .form-item {
display: flex;
align-items: center;
margin-bottom: 10px;
gap: 10px;
span {
color: #606266;
font-size: 14px;
} }
} }
.card-box-item-container {
display: grid;
grid-template-columns: repeat(3, minmax(100px, 1fr));
gap: 10px;
}
</style> </style>
<template>
<div
class="out-of-stock-statistics-page flex-column card h-100 overflow-hidden"
>
<div class="header-filter-form">
<ElForm v-enter-submit="search" :inline="true">
<ElFormItem label="缺货仓库">
<ElSelect
v-model="searchForm.warehouseId"
style="width: 180px"
placeholder="请选择仓库"
>
<ElOption
v-for="item in warehouseList"
:key="item.id"
:label="item.name"
:value="item.id"
></ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem label="库存SKU">
<ElInput
v-model="searchForm.warehouseSku"
clearable
style="width: 180px"
placeholder="请输入库存SKU"
></ElInput>
</ElFormItem>
<ElFormItem label="款号">
<ElInput
v-model="searchForm.productNo"
clearable
style="width: 180px"
placeholder="请输入款号"
></ElInput>
</ElFormItem>
<ElFormItem label="商品名称">
<ElInput
v-model="searchForm.skuName"
clearable
style="width: 180px"
placeholder="请输入商品名称"
></ElInput>
</ElFormItem>
<ElFormItem>
<ElButton type="primary" @click="search"> 查询 </ElButton>
</ElFormItem>
<ElFormItem>
<ElButton type="success" @click="exportData"> 导出 </ElButton>
</ElFormItem>
</ElForm>
</div>
<div class="table-content flex-1 flex-column overflow-hidden">
<div v-loading="loading" class="table-list flex-1 overflow-hidden">
<TableView
:selectionable="true"
:serial-numberable="true"
:paginated-data="tableData"
:columns="tableColumns"
@selection-change="handleSelectionChange"
>
<template #image="{ row }">
<el-image
v-if="row.imageUrl"
:src="row.imageUrl"
style="width: 60px; height: 60px"
:preview-src-list="[row.imageUrl]"
fit="cover"
></el-image>
<span v-else>暂无图片</span>
</template>
</TableView>
</div>
<ElPagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[50, 100, 200, 300, 500]"
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
style="margin: 10px auto 0; text-align: right"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></ElPagination>
</div>
</div>
</template>
<script setup lang="tsx">
import { computed, ref, onMounted } from 'vue'
import TableView from '@/components/TableView.vue'
import usePageList from '@/utils/hooks/usePageList'
import { loadWarehouseListApi } from '@/api/podCnOrder'
import type { WarehouseListData } from '@/types/api/podCnOrder'
import { SearchForm, OutOfStockItem } from '@/types/api/outOfStockStatistics'
import { getOutOfStockStatisticsListApi } from '@/api/outOfStockStis'
import ImageView from '@/components/ImageView.vue'
const searchForm = ref<SearchForm>({
warehouseId: '',
warehouseSku: '',
productNo: '',
skuName: '',
})
const warehouseList = ref<WarehouseListData[]>([])
// 表格列配置
const tableColumns = computed(() => {
return [
{
label: '图片',
prop: 'variantImage',
width: 100,
align: 'center',
slot: 'image',
render: (item: OutOfStockItem) => (
<ImageView
src={item.variantImage}
width="40px"
height="40px"
/>
),
},
{
label: '缺货仓库',
prop: 'warehouseName',
width: 120,
align: 'center',
},
{
label: '库位',
prop: 'locationCode',
width: 120,
align: 'center',
},
{
label: '库存SKU',
prop: 'warehouseSku',
width: 150,
align: 'center',
},
{
label: '款号',
prop: 'productNo',
width: 120,
align: 'center',
},
{
label: '商品名称',
prop: 'skuName',
minWidth: 200,
align: 'left',
},
{
label: '币种',
prop: 'currency',
width: 80,
align: 'center',
},
{
label: '商品成本价',
prop: 'costPrice',
width: 120,
align: 'right',
render: (item: OutOfStockItem) => (
<span>{item.costPrice ? item.costPrice.toFixed(2) : ''}</span>
),
},
{
label: '缺货数量',
prop: 'outOfStockQuantity',
width: 100,
align: 'right',
},
{
label: '销售数量',
prop: 'salesQuantity',
width: 100,
align: 'right',
},
{
label: '库存SPU',
prop: 'warehouseSpu',
width: 150,
align: 'center',
},
{
label: '最长缺货天数',
prop: 'longestDelayDays',
width: 120,
align: 'right',
},
{
label: '库存数量',
prop: 'inventory',
width: 100,
align: 'right',
},
{
label: '占用数量',
prop: 'occupyInventory',
width: 100,
align: 'right',
},
{
label: '冻结数量',
prop: 'freezeInventory',
width: 100,
align: 'right',
},
{
label: '采购未入数量',
prop: 'purchaseNotInQuantity',
width: 120,
align: 'right',
},
]
})
const {
loading,
currentPage,
pageSize,
total,
data: tableData,
refresh: search,
onCurrentPageChange: handleCurrentChange,
onPageSizeChange: handleSizeChange,
} = usePageList({
query: (page, pageSize) =>
getOutOfStockStatisticsListApi(
{
...searchForm.value,
},
page,
pageSize,
).then((res) => res.data) as never,
initLoad: false,
})
// 加载仓库列表
const loadWarehouseList = async () => {
try {
const res = await loadWarehouseListApi()
if (res.code !== 200) return
warehouseList.value = res.data || []
searchForm.value.warehouseId = warehouseList.value[0].id
} catch (e) {
console.error(e)
}
}
// 导出数据
const exportData = async () => {}
// 处理表格选择变化
const selectedRows = ref<OutOfStockItem[]>([])
const handleSelectionChange = (val: OutOfStockItem[]) => {
selectedRows.value = val
}
onMounted(async () => {
await loadWarehouseList()
search()
})
</script>
<style lang="scss" scoped>
.out-of-stock-statistics-page {
.header-filter-form {
:deep(.el-form-item) {
margin-right: 14px;
margin-bottom: 10px;
}
}
.table-content {
.table-list {
border: 1px solid #ebeef5;
border-radius: 4px;
}
}
}
</style>
...@@ -172,7 +172,16 @@ async function getData() { ...@@ -172,7 +172,16 @@ async function getData() {
...pagination.value, ...pagination.value,
...searchForm.value, ...searchForm.value,
}) })
leftData.value = res.data.records const sortedRecords = res.data.records.sort((a, b) => {
const aUsable = Number((a as WarehouseWarning & { usableInventory?: number | string }).usableInventory) || 0
const bUsable = Number((b as WarehouseWarning & { usableInventory?: number | string }).usableInventory) || 0
const aIsLow = aUsable < 100
const bIsLow = bUsable < 100
if (aIsLow && !bIsLow) return -1
if (!aIsLow && bIsLow) return 1
return 0
})
leftData.value = sortedRecords
pagination.value.total = res.data.total pagination.value.total = res.data.total
if (leftData.value.length) { if (leftData.value.length) {
getDetail(leftData.value[0].id) getDetail(leftData.value[0].id)
...@@ -292,6 +301,16 @@ const getWarehouse = async () => { ...@@ -292,6 +301,16 @@ const getWarehouse = async () => {
const { data } = await warehouseInfoGetAll() const { data } = await warehouseInfoGetAll()
warehouseList.value = data warehouseList.value = data
} }
const getRowClassName = (row: { row: WarehouseWarning }) => {
const rowData = row.row as WarehouseWarning & { usableInventory?: number | string }
const usableInventory = Number(rowData.usableInventory) || 0
if (usableInventory < 100) {
return 'low-inventory-row'
}
return ''
}
getData() getData()
getWarehouse() getWarehouse()
...@@ -509,6 +528,7 @@ useEnterKeyTrigger({ ...@@ -509,6 +528,7 @@ useEnterKeyTrigger({
height="100%" height="100%"
:data="leftData" :data="leftData"
border border
:row-class-name="getRowClassName"
@current-change="clickItem" @current-change="clickItem"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
> >
...@@ -831,6 +851,25 @@ useEnterKeyTrigger({ ...@@ -831,6 +851,25 @@ useEnterKeyTrigger({
} }
} }
::v-deep(.el-table) {
.low-inventory-row {
&:hover {
background-color: #f56c6c !important;
color: #fff !important;
}
td {
background-color: #f56c6c !important;
color: #fff !important;
}
&:hover td {
background-color: #f56c6c !important;
color: #fff !important;
}
}
}
.bottom-table { .bottom-table {
height: 100%; height: 100%;
background: white; background: white;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment