Commit ee1e96d4 by linjinhong

Merge branch 'dev_product_supply' of…

Merge branch 'dev_product_supply' of http://47.122.114.111:9999/qinjianhui/factory_front into dev_product_supply
parents 441b6295 71bc2511
......@@ -34,7 +34,6 @@ declare module 'vue' {
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
......
......@@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --host",
"build": "npm run lint && vue-tsc && vite build",
"preview": "vite preview",
"lint": "vue-tsc --noEmit && eslint"
......
......@@ -4,7 +4,7 @@ import { showError } from '@/utils/ui.ts'
const axios = Axios.create({
baseURL: import.meta.env.VITE_API_BASE,
timeout: 300000, //凯哥让改的超时时间
timeout: 30 * 60 * 1000, //半小时
})
const TOKEN_KEY = 'token'
......
......@@ -48,3 +48,14 @@ export function addExternalAuthorisationApi(
) {
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, BaseRespData } 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 },
)
}
export function exportOutOfStockStatisticsListApi(data: {
exportAll: boolean
indexes?: number[]
}) {
return axios.post<never, BaseRespData<never>>(
'stockOutStatistics/exportStockOutStatistics',
data,
)
}
......@@ -85,6 +85,23 @@ export function getCardOrderList(
},
)
}
export function uploadPRNFile(
id: number,
data: FormData,
) {
return axios.post<never, BaseRespData<string>>(
`/factory/podBatchDownload/uploadPRNFile?id=${id}`,
data
)
}
export function updatePRNDownloadStatus(
id: number
) {
return axios.get<never, BaseRespData<string>>(
`/factory/podBatchDownload/updatePRNDownloadStatus?id=${id}`
)
}
export function confirmOrderApi(
data: number[],
productionClient: string,
......@@ -556,10 +573,15 @@ export function changeToFinished(ids: string) {
)
}
export function reissueOrderApi(ids: string) {
return axios.get<never, BaseRespData<never>>(
export function reissueOrderApi(data: {
ids:string
reissueType:number
trackingNumber?:string
expressSheet:string
}) {
return axios.post<never, BaseRespData<never>>(
`factory/podJomallOrderUs/reissueOrder`,
{ params: { ids } },
data,
)
}
export function OrderCnReceiverAddress(idList: number[]) {
......
......@@ -163,6 +163,13 @@ const router = createRouter({
component: CustomersPage,
},
{
path: '/supply/out-of-stock-statistics',
meta: {
title: '缺货统计',
},
component: () => import('@/views/supply/OutOfStockStatistics.vue')
},
{
path: '/system/delivery-note',
meta: {
title: '定制发货单',
......
......@@ -148,19 +148,24 @@ const menu: MenuItem[] = [
},
],
},
{
index: '12',
id: 4,
index: '14',
id: 14,
label: '供应',
children: [
{
index: '/supply/supplierManagement',
label: '缺货统计',
index: '/supply/out-of-stock-statistics',
id: 1,
},
{
index: '/supply/supplierManagement',
id: 2,
label: '供应商管理',
},
],
},
{
index: '11',
id: 3,
......
export interface OutOfStockItem {
id?: number
variantImage?: string
warehouseName?: string
locationCode?: string
warehouseSku?: string
productNo?: string
skuName?: string
currency?: string
costPrice?: number
stockOutNum?: number
salesNum?: number
warehouseSpu?: string
inventory?: number
occupyInventory?: number
freezeInventory?: number
purchaseNotInQuantity?: number
longestDelayDays?: number
sort?: number
}
export interface SearchForm {
warehouseId?: string | number
warehouseSku?: string
productNo?: string
skuName?: string
}
\ No newline at end of file
......@@ -48,8 +48,11 @@ export interface SearchForm {
export interface PodUsOrderListData {
id: number
thirdOrderNumber?: string
prnUrl?: string
factoryOrderNumber?: string
prnDownloadStatus?: boolean
shopNumber?: string
isUpload?: boolean
factoryOnlineId?: number | null
factoryNo?: number | null
factoryCode?: string | null
......
<script setup lang="ts">
import { ref } from 'vue'
import { Plus, Close } from '@element-plus/icons-vue'
import { reissueOrderApi, uploadExpressSheet } from '@/api/podUsOrder.ts'
import { PodUsOrderListData } from '@/types/api/podUsOrder.ts'
const dialogShow = ref(false)
const formRef = ref()
const open = () => {
form.value = {
reissueType: 1,
trackingNumber: '',
expressSheet: '',
file: null,
}
dialogShow.value = true
}
const emit = defineEmits(['success'])
const props = defineProps({
selection: {
type: Array,
default: () => [],
},
})
const formRules = {
reissueType: {
required: true,
message: '请选择补发方式',
trigger: 'change',
},
trackingNumber: {
required: true,
message: '请输入跟踪号',
trigger: 'blur',
},
file: {
required: true,
message: '请上传物流面单',
trigger: 'change',
},
}
interface Iform {
reissueType: number
trackingNumber: string
expressSheet: string
file: File | null
}
const form = ref<Iform>({
reissueType: 1,
trackingNumber: '',
expressSheet: '',
file: null,
})
defineExpose({ open })
const submit = async () => {
await formRef.value.validate()
if((props.selection as PodUsOrderListData[])[0].shipmentType===0 && form.value.reissueType!==1){
if(!form.value.expressSheet){
return ElMessage.warning('请上传物流面单')
}
}
await reissueOrderApi({
ids: (props.selection as PodUsOrderListData[]).map(e => e.id).join(),
reissueType: form.value.reissueType,
trackingNumber: form.value.trackingNumber,
expressSheet: form.value.expressSheet,
})
ElMessage.success('操作成功')
dialogShow.value = false
emit('success')
}
const radioChange = () => {
form.value.trackingNumber = ''
form.value.expressSheet = ''
form.value.file = null
}
const clearFile = () => {
form.value.file = null
form.value.expressSheet = ''
}
const createFormToUpload = () => {
const input = document.createElement('input')
input.type = 'file'
input.style.display = 'none'
input.accept = '.pdf'
input.name = 'file'
document.body.appendChild(input)
input.click()
input.onchange = async function() {
console.log(input.files)
if (input.files?.length !== 1) {
return ElMessage.warning('请上传一个面单')
}
form.value.file = input.files[0]
const fm = new FormData()
fm.append('file', input.files[0])
fm.append('trackingNumber', form.value.trackingNumber)
const res = await uploadExpressSheet(fm as never)
form.value.expressSheet = res.message || ''
}
}
</script>
<template>
<el-dialog v-model="dialogShow" title="补发" :close-on-click-modal="false">
<el-form v-if="selection.length" ref="formRef" :rules="formRules" :model="form">
<el-form-item required label="补发方式" prop="reissueType">
<el-radio-group
v-model="form.reissueType"
style="display: flex;flex-direction: column;align-items: flex-start;"
@change="radioChange">
<el-radio :label="1">原单补发</el-radio>
<el-radio
:disabled="(selection as PodUsOrderListData[])[0].shipmentType===0 && selection.length>1"
:label="2">换单补发,取消原单物流
</el-radio>
<el-radio
:disabled="(selection as PodUsOrderListData[])[0].shipmentType===0 && selection.length>1"
:label="3">换单补发,不取消原单物流
</el-radio>
</el-radio-group>
</el-form-item>
<span v-if="(selection as PodUsOrderListData[])[0].shipmentType===0 && selection.length>1" class="tip">自有物流选择选项2和选项3请单个操作,不支持批量操作;</span>
<el-form-item
v-if="(selection as PodUsOrderListData[])[0].shipmentType===0 && form.reissueType!==1" required
label="物流信息" prop="trackingNumber">
<div class="form-content">
<div class="content-item">
<div class="label">跟踪号</div>
<el-input v-model="form.trackingNumber" style="height: 40px" placeholder="请输入跟踪号"></el-input>
</div>
<div v-if="form.trackingNumber" class="content-item">
<div class="label">物流面单</div>
<div class="upload" @click="createFormToUpload">
<el-icon>
<Plus></Plus>
</el-icon>
</div>
<div v-if="form.file" class="file">
<div class="name">{{ form.file.name }}</div>
<div class="close" @click="clearFile">
<el-icon>
<Close></Close>
</el-icon>
</div>
</div>
</div>
</div>
</el-form-item>
<el-form-item v-if="(selection as PodUsOrderListData[])[0].shipmentType===0 && form.reissueType!==1">
<div class="tip-2">自有物流请联系客户提供新跟踪号和面单</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogShow=false">取消</el-button>
<el-button type="primary" @click="submit">确定</el-button>
</template>
</el-dialog>
</template>
<style scoped lang="scss">
.tip {
color: red;
position: relative;
top: -14px;
left: 79px;
}
.form-content, .content-item {
display: flex;
margin-right: 25px;
.upload {
border: 1px darkgray solid;
width: 50px;
cursor: pointer;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
}
.file {
display: flex;
margin-left: 25px;
align-items: center;
gap: 6px;
.close {
cursor: pointer;
color: red;
}
}
.label {
width: 70px;
}
}
.tip-2{
color: red;
margin-left: 13px;
position: relative;
top: -9px;
}
</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"
>
</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>
<ElDialog
v-model="exportVisible"
title="导出选项"
width="500px"
:close-on-click-modal="false"
>
<el-form :model="exportForm" label-width="80px">
<el-form-item label="" prop="resource">
<el-radio-group v-model="exportForm.resource">
<el-radio label="currentPage">导出本页</el-radio>
<el-radio label="selectedRows">导出选中</el-radio>
<el-radio label="all">全部</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="exportVisible = false">取消</el-button>
<el-button
:loading="exportLoading"
type="primary"
@click="submitExportForm"
>确认</el-button
>
</span>
</template>
</ElDialog>
</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,
exportOutOfStockStatisticsListApi,
} from '@/api/outOfStockStis'
import ImageView from '@/components/ImageView.vue'
import { filePath } from '@/api/axios'
const searchForm = ref<SearchForm>({
warehouseId: '',
warehouseSku: '',
productNo: '',
skuName: '',
})
const warehouseList = ref<WarehouseListData[]>([])
const exportVisible = ref(false)
const exportForm = ref({
resource: null,
})
const exportLoading = ref(false)
// 表格列配置
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: 'stockOutNum',
width: 100,
align: 'right',
},
{
label: '销售数量',
prop: 'salesNum',
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<OutOfStockItem>({
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 () => {
exportForm.value.resource = null
exportVisible.value = true
}
const submitExportForm = async () => {
if (!exportForm.value.resource) {
return ElMessage.error('请选择导出类型')
}
const params: { exportAll: boolean; indexes?: number[] } = {
exportAll: false,
indexes: [],
}
if (exportForm.value.resource === 'currentPage') {
params.exportAll = false
params.indexes = undefined
} else if (exportForm.value.resource === 'selectedRows') {
if (!selectedRows.value.length) {
return ElMessage.error('请选择要导出的数据')
}
params.exportAll = false
params.indexes = selectedRows.value.map((el: OutOfStockItem) =>
Number(el.sort),
)
} else if (exportForm.value.resource === 'all') {
params.exportAll = true
params.indexes = undefined
}
try {
const res = await exportOutOfStockStatisticsListApi({
...searchForm.value,
...params,
})
window.open(filePath + res.message, '_blank')
exportVisible.value = false
} catch (e) {
console.error(e)
}
}
// 处理表格选择变化
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>
......@@ -20,6 +20,7 @@ import { ref, computed } from 'vue'
import SplitDiv from '@/components/splitDiv/splitDiv.vue'
import { filePath } from '@/api/axios.ts'
import { useEnterKeyTrigger } from '@/utils/hooks/useEnterKeyTrigger.ts'
import { getInventoryLowerLimitApi } from '@/api/externalAuth'
const searchForm = ref({
warehouseId: '',
......@@ -172,7 +173,24 @@ async function getData() {
...pagination.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
if (leftData.value.length) {
getDetail(leftData.value[0].id)
......@@ -292,8 +310,17 @@ const getWarehouse = async () => {
const { data } = await warehouseInfoGetAll()
warehouseList.value = data
}
getData()
getWarehouse()
const getRowClassName = (row: { row: WarehouseWarning }) => {
const rowData = row.row as WarehouseWarning & {
usableInventory?: number | string
}
const usableInventory = Number(rowData.usableInventory) || 0
if (usableInventory < inventoryLowerLimit.value) {
return 'low-inventory-row'
}
return ''
}
/**
* @description: 页面添加回车监听
......@@ -310,6 +337,23 @@ useEnterKeyTrigger({
getData()
},
})
const inventoryLowerLimit = ref<number>(100)
const getInventoryLowerLimit = async () => {
try {
const res = await getInventoryLowerLimitApi()
if (res.code !== 200) {
return
}
inventoryLowerLimit.value = res.data
} catch (error) {
console.log(error)
}
}
onMounted(async () => {
getData()
getWarehouse()
getInventoryLowerLimit()
})
</script>
<template>
......@@ -509,6 +553,7 @@ useEnterKeyTrigger({
height="100%"
:data="leftData"
border
:row-class-name="getRowClassName"
@current-change="clickItem"
@selection-change="handleSelectionChange"
>
......@@ -831,6 +876,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 {
height: 100%;
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