feat (router): 新增采购详情路由,优化预警数量管理功能

在管理员模块与商家模块中新增采购详情页面路由。
升级预警数量管理组件,优化交互体验与功能逻辑。
This commit is contained in:
pikachu1995@126.com
2026-07-22 15:52:15 +08:00
parent e91d783e22
commit c83d715234
26 changed files with 3569 additions and 206 deletions

View File

@@ -0,0 +1,38 @@
import {
getRequest,
postRequest,
putRequest,
deleteRequest,
} from "@/libs/axios";
// ========== 采购单(只读) ==========
export const getProcurementOrderPage = params => getRequest("/procurement/order", params);
export const getProcurementOrderDetail = id => getRequest(`/procurement/order/${id}`);
// ========== 采购入库单(只读) ==========
export const getProcurementInboundPage = params => getRequest("/procurement/inbound", params);
export const getProcurementInboundDetail = id => getRequest(`/procurement/inbound/${id}`);
export const getProcurementInboundItems = id => getRequest(`/procurement/inbound/${id}/items`);
// ========== 盘点单(只读) ==========
export const getInventoryCountPage = params => getRequest("/procurement/inventory-count/page", params);
export const getInventoryCountDetail = id => getRequest(`/procurement/inventory-count/${id}`);
export const getInventoryCountItemsPage = (id, params) =>
getRequest(`/procurement/inventory-count/${id}/items/page`, params);
export const downloadInventoryCountItems = id =>
getRequest(`/procurement/inventory-count/${id}/download`);
export const exportInventoryCountItems = id =>
getRequest(`/procurement/inventory-count/${id}/export`, {}, "blob");
// ========== 报损单(只读) ==========
export const getDamageReportPage = params => getRequest("/procurement/damage-report/page", params);
export const getDamageReportDetail = id => getRequest(`/procurement/damage-report/${id}`);
export const getDamageReportItems = id => getRequest(`/procurement/damage-report/${id}/items`);
// ========== 出入库原因 CRUD ==========
export const getStockReasonPage = params => getRequest("/procurement/reason", params);
export const addStockReason = params =>
postRequest("/procurement/reason", params, { "Content-Type": "application/json" });
export const updateStockReason = params =>
putRequest("/procurement/reason", params, { "Content-Type": "application/json" });
export const deleteStockReason = id => deleteRequest(`/procurement/reason/${id}`);

View File

@@ -454,6 +454,30 @@ export const otherRouter = {
name: "goodsSalesSummaryReport", name: "goodsSalesSummaryReport",
meta: { title: "商品销售汇总报表" }, meta: { title: "商品销售汇总报表" },
component: () => import("@/views/statistics/goods-sales-summary.vue") component: () => import("@/views/statistics/goods-sales-summary.vue")
},
{
path: "procurement/purchase-order/detail",
title: "采购单详情",
name: "manager-procurement-order-detail",
component: () => import("@/views/procurement/purchase-order/detail.vue")
},
{
path: "procurement/inbound/detail",
title: "入库单详情",
name: "manager-procurement-inbound-detail",
component: () => import("@/views/procurement/inbound/detail.vue")
},
{
path: "procurement/inventory-count/detail",
title: "盘点单详情",
name: "manager-inventory-count-detail",
component: () => import("@/views/procurement/inventory-count/detail.vue")
},
{
path: "procurement/damage-report/detail",
title: "报损单详情",
name: "manager-damage-report-detail",
component: () => import("@/views/procurement/damage-report/detail.vue")
} }
] ]
}; };

View File

@@ -0,0 +1,91 @@
export const PROCUREMENT_STATUS = {
DRAFT: "待提交",
SUBMITTED: "已提交",
PENDING_INBOUND: "待入库",
PARTIAL_INBOUND: "部分入库",
CLOSED: "已关闭",
COMPLETED: "已完成",
REJECTED: "已拒绝",
};
export const PROCUREMENT_STATUS_TAG = {
DRAFT: "info",
SUBMITTED: "warning",
PENDING_INBOUND: "primary",
PARTIAL_INBOUND: "warning",
CLOSED: "info",
COMPLETED: "success",
REJECTED: "danger",
};
export const DAMAGE_STATUS = {
DRAFT: "待提交",
SUBMITTED: "待审核",
APPROVED: "已通过",
REJECTED: "已驳回",
CANCELLED: "已作废",
COMPLETED: "已完成",
};
export const DAMAGE_STATUS_TAG = {
DRAFT: "info",
SUBMITTED: "warning",
APPROVED: "primary",
REJECTED: "danger",
CANCELLED: "info",
COMPLETED: "success",
};
export const MARKET_ENABLE = {
UPPER: "上架",
DOWN: "下架",
};
export const STOCK_REASON_CATEGORY = {
INBOUND: "入库",
OUTBOUND: "出库",
DAMAGE: "报损",
};
export function stockReasonCategoryText(category) {
return STOCK_REASON_CATEGORY[category] || category || "-";
}
export function procurementStatusText(status) {
return PROCUREMENT_STATUS[status] || status || "-";
}
export function procurementStatusTag(status) {
return PROCUREMENT_STATUS_TAG[status] || "info";
}
export function damageStatusText(status) {
return DAMAGE_STATUS[status] || status || "-";
}
export function damageStatusTag(status) {
return DAMAGE_STATUS_TAG[status] || "info";
}
export function formatMoney(val) {
if (val === null || val === undefined || val === "") return "0.00";
return Number(val).toFixed(2);
}
export function exportCsv(filename, headers, rows) {
const escape = (v) => {
const s = v == null ? "" : String(v);
if (s.includes(",") || s.includes('"') || s.includes("\n")) {
return `"${s.replace(/"/g, '""')}"`;
}
return s;
};
const lines = [headers.map(escape).join(",")];
rows.forEach((row) => lines.push(row.map(escape).join(",")));
const blob = new Blob(["\uFEFF" + lines.join("\n")], { type: "text/csv;charset=utf-8;" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
}

View File

@@ -0,0 +1,83 @@
<template>
<div class="search">
<el-card>
<template #header>
<div class="card-header">
<span>报损单详情</span>
<el-button @click="$router.back()">返回</el-button>
</div>
</template>
<el-descriptions v-if="detail.id" :column="2" border>
<el-descriptions-item label="报损单号">{{ detail.sn }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="damageStatusTag(detail.status)">{{ damageStatusText(detail.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建人">{{ detail.makerName || detail.createBy || "-" }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detail.createTime || "-" }}</el-descriptions-item>
<el-descriptions-item label="审核人">{{ detail.auditorName || "-" }}</el-descriptions-item>
<el-descriptions-item label="审核时间">{{ detail.auditTime || "-" }}</el-descriptions-item>
<el-descriptions-item label="报损日期">{{ detail.damageDate }}</el-descriptions-item>
<el-descriptions-item label="总数量">{{ detail.totalQuantity }}</el-descriptions-item>
<el-descriptions-item label="总金额">{{ formatMoney(detail.totalAmount) }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detail.remark || "-" }}</el-descriptions-item>
</el-descriptions>
<el-table :data="items" border class="mt_10 item-table" style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" class-name="goods-name-col" width="200">
<template #default="{ row }">{{ row.goodsName }}</template>
</el-table-column>
<el-table-column prop="skuId" label="SKU ID" class-name="sku-id-col" width="200">
<template #default="{ row }">{{ row.skuId }}</template>
</el-table-column>
<el-table-column prop="quantity" label="报损数量" width="90" align="center" />
<el-table-column label="单价" width="110" align="right">
<template #default="{ row }">{{ formatMoney(row.unitPrice) }}</template>
</el-table-column>
<el-table-column label="金额" width="100" align="right">
<template #default="{ row }">{{ formatMoney(row.amount) }}</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script>
import { getDamageReportDetail, getDamageReportItems } from "@/api/procurement";
import { damageStatusText, damageStatusTag, formatMoney } from "../constants";
export default {
name: "manager-damage-report-detail",
data() {
return { detail: {}, items: [] };
},
mounted() {
const id = this.$route.query.id;
if (id) {
getDamageReportDetail(id).then((res) => {
if (res.success) this.detail = res.result || {};
});
getDamageReportItems(id).then((res) => {
if (res.success) this.items = res.result || [];
});
}
},
methods: { damageStatusText, damageStatusTag, formatMoney },
};
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-table :deep(.sku-id-col .cell) {
white-space: normal;
word-break: break-all;
line-height: 1.4;
}
.item-table :deep(.goods-name-col .cell) {
white-space: normal;
word-break: break-all;
line-height: 1.4;
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<div class="search">
<el-card>
<el-form :model="searchForm" inline label-width="80px">
<el-form-item label="店铺ID">
<el-input v-model="searchForm.storeId" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" clearable style="width: 160px">
<el-option v-for="(label, value) in DAMAGE_STATUS" :key="value" :label="label" :value="value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="data" border style="width: 100%">
<el-table-column prop="sn" label="报损单号" width="220" show-overflow-tooltip />
<el-table-column prop="storeId" label="店铺ID" width="180" show-overflow-tooltip />
<el-table-column prop="totalQuantity" label="数量" width="90" align="center" />
<el-table-column label="金额" width="110" align="right">
<template #default="{ row }">{{ formatMoney(row.totalAmount) }}</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="damageStatusTag(row.status)">{{ damageStatusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ row }">
<a class="link-text" @click="goDetail(row)">详情</a>
</template>
</el-table-column>
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:total="total"
layout="total, prev, pager, next"
size="small"
@current-change="getDataList"
@size-change="getDataList"
/>
</div>
</el-card>
</div>
</template>
<script>
import { getDamageReportPage } from "@/api/procurement";
import { DAMAGE_STATUS, damageStatusText, damageStatusTag, formatMoney } from "../constants";
export default {
name: "manager-damage-report-list",
data() {
return {
DAMAGE_STATUS,
loading: false,
data: [],
total: 0,
searchForm: { pageNumber: 1, pageSize: 10, storeId: "", status: "" },
};
},
mounted() {
this.getDataList();
},
methods: {
damageStatusText,
damageStatusTag,
formatMoney,
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.loading = true;
getDamageReportPage(this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
goDetail(row) {
this.$router.push({ name: "manager-damage-report-detail", query: { id: row.id } });
},
},
};
</script>

View File

@@ -0,0 +1,78 @@
<template>
<div class="search">
<el-card>
<template #header>
<div class="card-header">
<span>入库单详情</span>
<el-button @click="$router.back()">返回</el-button>
</div>
</template>
<el-descriptions v-if="detail.id" :column="2" border>
<el-descriptions-item label="入库单号">{{ detail.inboundSn }}</el-descriptions-item>
<el-descriptions-item label="采购单ID">{{ detail.procurementOrderId }}</el-descriptions-item>
<el-descriptions-item label="本次入库数量">{{ totalInboundQuantity }}</el-descriptions-item>
<el-descriptions-item label="入库成本">{{ formatMoney(detail.totalCost) }}</el-descriptions-item>
<el-descriptions-item label="操作人">{{ detail.operatorName }}</el-descriptions-item>
</el-descriptions>
<el-table :data="items" border class="mt_10 item-table" style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" class-name="goods-name-col" width="200">
<template #default="{ row }">{{ row.goodsName }}</template>
</el-table-column>
<el-table-column prop="skuId" label="SKU ID" class-name="sku-id-col" width="200">
<template #default="{ row }">{{ row.skuId }}</template>
</el-table-column>
<el-table-column prop="inboundQuantity" label="入库数量" width="90" align="center" />
</el-table>
</el-card>
</div>
</template>
<script>
import { getProcurementInboundDetail, getProcurementInboundItems } from "@/api/procurement";
import { formatMoney } from "../constants";
export default {
name: "manager-procurement-inbound-detail",
data() {
return { detail: {}, items: [] };
},
computed: {
totalInboundQuantity() {
return (this.items || []).reduce(
(sum, item) => sum + (Number(item.inboundQuantity) || 0),
0
);
},
},
mounted() {
const id = this.$route.query.id;
if (id) {
getProcurementInboundDetail(id).then((res) => {
if (res.success) this.detail = res.result || {};
});
getProcurementInboundItems(id).then((res) => {
if (res.success) this.items = res.result || [];
});
}
},
methods: { formatMoney },
};
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-table :deep(.sku-id-col .cell) {
white-space: normal;
word-break: break-all;
line-height: 1.4;
}
.item-table :deep(.goods-name-col .cell) {
white-space: normal;
word-break: break-all;
line-height: 1.4;
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<div class="search">
<el-card>
<el-form :model="searchForm" inline label-width="80px">
<el-form-item label="入库单号">
<el-input v-model="searchForm.inboundSn" clearable style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="data" border style="width: 100%">
<el-table-column prop="inboundSn" label="入库单号" width="220" show-overflow-tooltip />
<el-table-column prop="storeId" label="店铺ID" width="180" show-overflow-tooltip />
<el-table-column prop="confirmedQuantity" label="已入库" width="90" align="center" />
<el-table-column label="成本" width="110" align="right">
<template #default="{ row }">{{ formatMoney(row.totalCost) }}</template>
</el-table-column>
<el-table-column prop="operatorName" label="操作人" width="120" show-overflow-tooltip />
<el-table-column prop="inboundTime" label="入库时间" width="170" />
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ row }">
<a class="link-text" @click="goDetail(row)">详情</a>
</template>
</el-table-column>
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:total="total"
layout="total, prev, pager, next"
size="small"
@current-change="getDataList"
@size-change="getDataList"
/>
</div>
</el-card>
</div>
</template>
<script>
import { getProcurementInboundPage } from "@/api/procurement";
import { formatMoney } from "../constants";
export default {
name: "manager-procurement-inbound-list",
data() {
return {
loading: false,
data: [],
total: 0,
searchForm: { pageNumber: 1, pageSize: 10, inboundSn: "" },
};
},
mounted() {
this.getDataList();
},
methods: {
formatMoney,
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.loading = true;
getProcurementInboundPage(this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
goDetail(row) {
this.$router.push({ name: "manager-procurement-inbound-detail", query: { id: row.id } });
},
},
};
</script>

View File

@@ -0,0 +1,106 @@
<template>
<div class="search">
<el-card>
<template #header>
<div class="card-header">
<span>盘点单明细 - {{ sn }}</span>
<div>
<el-button type="primary" :loading="exporting" @click="handleExport">导出 Excel</el-button>
<el-button @click="$router.back()">返回</el-button>
</div>
</div>
</template>
<el-table v-loading="loading" :data="data" border class="item-table" style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" class-name="goods-name-col" width="200">
<template #default="{ row }">{{ row.goodsName }}</template>
</el-table-column>
<el-table-column prop="skuName" label="规格" width="120" show-overflow-tooltip />
<el-table-column prop="skuId" label="SKU ID" class-name="sku-id-col" width="200">
<template #default="{ row }">{{ row.skuId }}</template>
</el-table-column>
<el-table-column prop="quantity" label="库存数量" width="100" align="center" />
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:total="total"
layout="total, prev, pager, next"
size="small"
@current-change="getDataList"
@size-change="getDataList"
/>
</div>
</el-card>
</div>
</template>
<script>
import { getInventoryCountItemsPage, exportInventoryCountItems } from "@/api/procurement";
import { downloadBlob } from "@/utils/downloadBlob";
export default {
name: "manager-inventory-count-detail",
data() {
return {
loading: false,
exporting: false,
data: [],
total: 0,
sn: "",
searchForm: { pageNumber: 1, pageSize: 20 },
};
},
mounted() {
this.sn = this.$route.query.sn || "";
this.getDataList();
},
methods: {
getDataList() {
const id = this.$route.query.id;
if (!id) return;
this.loading = true;
getInventoryCountItemsPage(id, this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
handleExport() {
const id = this.$route.query.id;
if (!id) return;
this.exporting = true;
exportInventoryCountItems(id)
.then((blob) => {
downloadBlob(blob, `盘点单_${this.sn || id}.xlsx`);
})
.finally(() => {
this.exporting = false;
});
},
},
};
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-table :deep(.sku-id-col .cell) {
white-space: normal;
word-break: break-all;
line-height: 1.4;
}
.item-table :deep(.goods-name-col .cell) {
white-space: normal;
word-break: break-all;
line-height: 1.4;
}
</style>

View File

@@ -0,0 +1,66 @@
<template>
<div class="search">
<el-card>
<el-table v-loading="loading" :data="data" border style="width: 100%">
<el-table-column prop="sn" label="盘点单号" width="220" show-overflow-tooltip />
<el-table-column prop="storeId" label="店铺ID" width="180" show-overflow-tooltip />
<el-table-column prop="itemTotal" label="商品数" width="90" align="center" />
<el-table-column prop="makerName" label="制单人" width="120" show-overflow-tooltip />
<el-table-column prop="countTime" label="盘点时间" width="170" />
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ row }">
<a class="link-text" @click="goDetail(row)">详情</a>
</template>
</el-table-column>
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:total="total"
layout="total, prev, pager, next"
size="small"
@current-change="getDataList"
@size-change="getDataList"
/>
</div>
</el-card>
</div>
</template>
<script>
import { getInventoryCountPage } from "@/api/procurement";
export default {
name: "manager-inventory-count-list",
data() {
return {
loading: false,
data: [],
total: 0,
searchForm: { pageNumber: 1, pageSize: 10 },
};
},
mounted() {
this.getDataList();
},
methods: {
getDataList() {
this.loading = true;
getInventoryCountPage(this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
goDetail(row) {
this.$router.push({ name: "manager-inventory-count-detail", query: { id: row.id, sn: row.sn } });
},
},
};
</script>

View File

@@ -0,0 +1,92 @@
<template>
<div class="search">
<el-card>
<template #header>
<div class="card-header">
<span>采购单详情</span>
<el-button @click="$router.back()">返回</el-button>
</div>
</template>
<el-descriptions v-if="detail.id" :column="2" border>
<el-descriptions-item label="采购单号">{{ detail.orderSn }}</el-descriptions-item>
<el-descriptions-item label="店铺">{{ detail.storeName }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="procurementStatusTag(detail.status)">{{ procurementStatusText(detail.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="制单人">{{ detail.makerName }}</el-descriptions-item>
<el-descriptions-item label="审核人">{{ detail.auditorName || '-' }}</el-descriptions-item>
<el-descriptions-item label="审核时间">{{ detail.auditTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="总数量">{{ detail.totalQuantity }}</el-descriptions-item>
<el-descriptions-item label="总金额">{{ formatMoney(detail.totalAmount) }}</el-descriptions-item>
<el-descriptions-item label="出入库原因">{{ stockReasonText }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detail.remark || '-' }}</el-descriptions-item>
</el-descriptions>
<el-table :data="detail.items || []" border class="mt_10 item-table" style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" class-name="goods-name-col" width="200">
<template #default="{ row }">{{ row.goodsName }}</template>
</el-table-column>
<el-table-column prop="skuId" label="SKU ID" class-name="sku-id-col" width="200">
<template #default="{ row }">{{ row.skuId }}</template>
</el-table-column>
<el-table-column prop="quantity" label="采购数量" width="90" align="center" />
<el-table-column prop="receivedQuantity" label="已入库" width="80" align="center" />
<el-table-column label="含税单价" width="110" align="right">
<template #default="{ row }">{{ formatMoney(row.unitPriceWithTax) }}</template>
</el-table-column>
<el-table-column label="含税小计" width="100" align="right">
<template #default="{ row }">{{ formatMoney(row.subtotalWithTax) }}</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script>
import { getProcurementOrderDetail, getStockReasonPage } from "@/api/procurement";
import { procurementStatusText, procurementStatusTag, formatMoney } from "../constants";
export default {
name: "manager-procurement-order-detail",
data() {
return { detail: {}, reasonOptions: [] };
},
computed: {
stockReasonText() {
const reason = this.reasonOptions.find((item) => item.id === this.detail.stockReasonId);
return reason ? reason.reason : "-";
},
},
mounted() {
getStockReasonPage({ category: "INBOUND", pageNumber: 1, pageSize: 100 }).then((res) => {
if (res.success) {
this.reasonOptions = res.result.records || [];
}
});
const id = this.$route.query.id;
if (id) {
getProcurementOrderDetail(id).then((res) => {
if (res.success) this.detail = res.result || {};
});
}
},
methods: { procurementStatusText, procurementStatusTag, formatMoney },
};
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-table :deep(.sku-id-col .cell) {
white-space: normal;
word-break: break-all;
line-height: 1.4;
}
.item-table :deep(.goods-name-col .cell) {
white-space: normal;
word-break: break-all;
line-height: 1.4;
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<div class="search">
<el-card>
<el-form :model="searchForm" inline label-width="80px">
<el-form-item label="单据编号">
<el-input v-model="searchForm.orderSn" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="商品名称">
<el-input v-model="searchForm.goodsName" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" clearable style="width: 160px">
<el-option v-for="(label, value) in PROCUREMENT_STATUS" :key="value" :label="label" :value="value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="data" border style="width: 100%">
<el-table-column prop="orderSn" label="采购单号" width="220" show-overflow-tooltip />
<el-table-column prop="storeName" label="店铺" width="140" show-overflow-tooltip />
<el-table-column prop="totalQuantity" label="数量" width="90" align="center" />
<el-table-column label="金额" width="110" align="right">
<template #default="{ row }">{{ formatMoney(row.totalAmount) }}</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="procurementStatusTag(row.status)">{{ procurementStatusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column prop="auditTime" label="审核时间" width="170" />
<el-table-column prop="auditorName" label="审核人" width="120" show-overflow-tooltip />
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ row }">
<a class="link-text" @click="goDetail(row)">详情</a>
</template>
</el-table-column>
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:total="total"
layout="total, prev, pager, next"
size="small"
@current-change="getDataList"
@size-change="getDataList"
/>
</div>
</el-card>
</div>
</template>
<script>
import { getProcurementOrderPage } from "@/api/procurement";
import { PROCUREMENT_STATUS, procurementStatusText, procurementStatusTag, formatMoney } from "../constants";
export default {
name: "manager-procurement-order-list",
data() {
return {
PROCUREMENT_STATUS,
loading: false,
data: [],
total: 0,
searchForm: { pageNumber: 1, pageSize: 10, orderSn: "", goodsName: "", status: "" },
};
},
mounted() {
this.getDataList();
},
methods: {
procurementStatusText,
procurementStatusTag,
formatMoney,
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.loading = true;
getProcurementOrderPage(this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
goDetail(row) {
this.$router.push({ name: "manager-procurement-order-detail", query: { id: row.id } });
},
},
};
</script>

View File

@@ -0,0 +1,169 @@
<template>
<div class="search">
<el-card>
<el-form :model="searchForm" inline label-width="80px">
<el-form-item label="原因">
<el-input v-model="searchForm.reason" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="类别">
<el-select v-model="searchForm.category" clearable style="width: 160px">
<el-option label="入库" value="INBOUND" />
<el-option label="出库" value="OUTBOUND" />
<el-option label="报损" value="DAMAGE" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
</el-form-item>
</el-form>
<div class="operation padding-row">
<el-button type="primary" @click="openAdd">添加</el-button>
</div>
<el-table v-loading="loading" :data="data" border class="mt_10" style="width: 100%">
<el-table-column prop="reason" label="原因" width="240" show-overflow-tooltip />
<el-table-column label="类别" width="120" align="center">
<template #default="{ row }">{{ stockReasonCategoryText(row.category) }}</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="{ row }">
<a class="link-text" @click="openEdit(row)">编辑</a>
<span class="op-split">|</span>
<a class="link-text" @click="handleDelete(row)">删除</a>
</template>
</el-table-column>
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:total="total"
layout="total, prev, pager, next"
size="small"
@current-change="getDataList"
@size-change="getDataList"
/>
</div>
</el-card>
<el-dialog v-model="modalVisible" :title="modalTitle" width="480px" append-to-body>
<el-form ref="formRef" :model="form" label-width="80px">
<el-form-item label="原因" required>
<el-input v-model="form.reason" placeholder="出入库原因" />
</el-form-item>
<el-form-item label="类别" required>
<el-select v-model="form.category" style="width: 100%">
<el-option label="入库" value="INBOUND" />
<el-option label="出库" value="OUTBOUND" />
<el-option label="报损" value="DAMAGE" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="modalVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import {
getStockReasonPage,
addStockReason,
updateStockReason,
deleteStockReason,
} from "@/api/procurement";
import { stockReasonCategoryText } from "../constants";
export default {
name: "manager-stock-reason-manage",
data() {
return {
loading: false,
submitLoading: false,
modalVisible: false,
modalTitle: "添加原因",
data: [],
total: 0,
searchForm: { pageNumber: 1, pageSize: 10, reason: "", category: "" },
form: { id: "", reason: "", category: "INBOUND" },
};
},
mounted() {
this.getDataList();
},
methods: {
stockReasonCategoryText,
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.loading = true;
getStockReasonPage(this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
openAdd() {
this.modalTitle = "添加原因";
this.form = { id: "", reason: "", category: "INBOUND" };
this.modalVisible = true;
},
openEdit(row) {
this.modalTitle = "编辑原因";
this.form = { id: row.id, reason: row.reason, category: row.category };
this.modalVisible = true;
},
handleSubmit() {
if (!this.form.reason || !this.form.category) {
this.$Message.warning("请填写完整信息");
return;
}
this.submitLoading = true;
const req = this.form.id
? updateStockReason(this.form)
: addStockReason({ reason: this.form.reason, category: this.form.category });
req
.then((res) => {
if (res.success) {
this.$Message.success("保存成功");
this.modalVisible = false;
this.getDataList();
}
})
.finally(() => {
this.submitLoading = false;
});
},
handleDelete(row) {
this.$Modal.confirm({
title: "删除确认",
content: `确认删除原因「${row.reason}」?`,
onOk: () => {
deleteStockReason(row.id).then((res) => {
if (res.success) {
this.$Message.success("删除成功");
this.getDataList();
}
});
},
});
},
},
};
</script>
<style scoped>
.op-split {
margin: 0 6px;
color: #dcdfe6;
}
</style>

View File

@@ -0,0 +1,51 @@
import {
getRequest,
postRequest,
putRequest,
} from "@/libs/axios";
// ========== 采购单 ==========
export const getProcurementOrderPage = params => getRequest("/procurement/order", params);
export const getProcurementOrderStatusCount = params => getRequest("/procurement/order/status-count", params);
export const getProcurementOrderDetail = id => getRequest(`/procurement/order/${id}`);
export const createProcurementOrder = params =>
postRequest("/procurement/order", params, { "Content-Type": "application/json" });
export const submitProcurementOrder = id => putRequest(`/procurement/order/submit/${id}`);
export const auditProcurementOrder = (id, params) =>
putRequest(`/procurement/order/audit/${id}`, params, { "Content-Type": "application/json" });
export const closeProcurementOrder = id => putRequest(`/procurement/order/close/${id}`);
// ========== 采购入库单 ==========
export const getProcurementInboundPage = params => getRequest("/procurement/inbound", params);
export const getProcurementInboundDetail = id => getRequest(`/procurement/inbound/${id}`);
export const getProcurementInboundItems = id => getRequest(`/procurement/inbound/${id}/items`);
export const createProcurementInbound = params =>
postRequest("/procurement/inbound", params, { "Content-Type": "application/json" });
// ========== 盘点单 ==========
export const createInventoryCount = () => postRequest("/procurement/inventory-count");
export const getInventoryCountPage = params => getRequest("/procurement/inventory-count/page", params);
export const getInventoryCountDetail = id => getRequest(`/procurement/inventory-count/${id}`);
export const getInventoryCountItemsPage = (id, params) =>
getRequest(`/procurement/inventory-count/${id}/items/page`, params);
export const downloadInventoryCountItems = id =>
getRequest(`/procurement/inventory-count/${id}/download`);
export const exportInventoryCountItems = id =>
getRequest(`/procurement/inventory-count/${id}/export`, {}, "blob");
// ========== 报损单 ==========
export const getDamageReportPage = params => getRequest("/procurement/damage-report/page", params);
export const getDamageReportStatusCount = params => getRequest("/procurement/damage-report/status-count", params);
export const getDamageReportDetail = id => getRequest(`/procurement/damage-report/${id}`);
export const getDamageReportItems = id => getRequest(`/procurement/damage-report/${id}/items`);
export const createDamageReport = params =>
postRequest("/procurement/damage-report", params, { "Content-Type": "application/json" });
export const submitDamageReport = id => putRequest(`/procurement/damage-report/${id}/submit`);
export const approveDamageReport = id => putRequest(`/procurement/damage-report/${id}/approve`);
export const rejectDamageReport = (id, remark) =>
putRequest(`/procurement/damage-report/${id}/reject?remark=${encodeURIComponent(remark || "")}`);
export const cancelDamageReport = id => putRequest(`/procurement/damage-report/${id}/cancel`);
export const completeDamageReport = id => putRequest(`/procurement/damage-report/${id}/complete`);
// ========== 出入库原因 ==========
export const getStockReasonList = params => getRequest("/procurement/reason", params);

View File

@@ -197,6 +197,42 @@ export const otherRouter = {
name: "goodsSalesSummaryReport", name: "goodsSalesSummaryReport",
meta: { title: "商品销售汇总报表" }, meta: { title: "商品销售汇总报表" },
component: () => import("@/views/statistics/goods-sales-summary.vue") component: () => import("@/views/statistics/goods-sales-summary.vue")
},
{
path: "procurement/purchase-order/operation",
title: "采购单",
name: "procurement-purchase-order-operation",
component: () => import("@/views/procurement/purchase-order/operation.vue")
},
{
path: "procurement/inbound/operation",
title: "采购入库",
name: "procurement-inbound-operation",
component: () => import("@/views/procurement/inbound/operation.vue")
},
{
path: "procurement/inventory-count/detail",
title: "盘点明细",
name: "procurement-inventory-count-detail",
component: () => import("@/views/procurement/inventory-count/detail.vue")
},
{
path: "procurement/damage-report/operation",
title: "报损单",
name: "procurement-damage-report-operation",
component: () => import("@/views/procurement/damage-report/operation.vue")
},
{
path: "alertQuantityWarnList",
title: "预警商品",
name: "alertQuantityWarnList",
component: () => import("@/views/goods/goods-seller/alertQuantityWarn.vue")
},
{
path: "alertQuantityWarnSetting",
title: "设置预警",
name: "alertQuantityWarnSetting",
component: () => import("@/views/goods/goods-seller/alertQuantityWarn.vue")
} }
] ]
}; };

View File

@@ -34,8 +34,7 @@
</el-form> </el-form>
</div> </div>
<el-tabs v-model="stockType" @tab-click="switchTabs"> <div>
<el-tab-pane label="商品库存管理" name="stockManage">
<div class="operation padding-row"> <div class="operation padding-row">
<el-button type="primary" class="export" @click="exportStock">批量导出</el-button> <el-button type="primary" class="export" @click="exportStock">批量导出</el-button>
<el-button class="export" @click="openImportStock">批量导入</el-button> <el-button class="export" @click="openImportStock">批量导入</el-button>
@@ -84,46 +83,7 @@
<template #default="{ row }">{{ row.quantity || 0 }}</template> <template #default="{ row }">{{ row.quantity || 0 }}</template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-tab-pane> </div>
<el-tab-pane label="预警商品" name="warnList">
<el-table v-loading="loading" border :data="warnData" class="mt_10" style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" min-width="400" show-overflow-tooltip />
<el-table-column label="库存" min-width="120">
<template #default="{ row }">{{ row.quantity || 0 }}</template>
</el-table-column>
<el-table-column label="预警值" min-width="120">
<template #default="{ row }">{{ row.alertQuantity || 0 }}</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row }">
<a class="link-text" @click="openUpdataStockModal(row)">库存</a>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="设置预警" name="warnSetting">
<el-table v-loading="loading" border :data="skuAllData" class="mt_10" style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" />
<el-table-column label="库存" width="200">
<template #default="{ row }">{{ row.quantity || 0 }}</template>
</el-table-column>
<el-table-column label="预警值" width="200">
<template #default="{ row }">
<el-input
v-model="row.alertQuantity"
type="number"
clearable
placeholder="请输入预警库存"
@blur="updateWarnStock(row)"
@change="checkVal(row)"
/>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
<div class="mt_10" style="display: flex; justify-content: flex-end"> <div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination <el-pagination
@@ -139,36 +99,6 @@
</div> </div>
</el-card> </el-card>
<el-dialog v-model="updateStockModalVisible" title="更新库存" width="610px" :close-on-click-modal="false">
<el-table :data="stockList" border class="mt_10" style="width: 100%">
<el-table-column label="sku规格" min-width="120">
<template #default="{ row }">{{ row.simpleSpecs }}</template>
</el-table-column>
<el-table-column label="审核状态" width="130">
<template #default="{ row }">
<el-tag
:type="
row.authFlag === 'PASS' ? 'success' : row.authFlag === 'TOBEAUDITED' ? 'primary' : 'danger'
"
>
{{
row.authFlag === "TOBEAUDITED" ? "待审核" : row.authFlag === "PASS" ? "通过" : "审核拒绝"
}}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center">
<template #default="{ row }">
<el-input-number v-model="row.quantity" :min="0" />
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="updateStockModalVisible = false">取消</el-button>
<el-button type="primary" @click="updateStock">更新</el-button>
</template>
</el-dialog>
<el-dialog v-model="importModal" title="导入商品信息" :close-on-click-modal="false"> <el-dialog v-model="importModal" title="导入商品信息" :close-on-click-modal="false">
<div v-loading="spinShow" style="text-align: center"> <div v-loading="spinShow" style="text-align: center">
<el-upload drag :before-upload="handleUpload" :show-file-list="false" accept=".xlsx,.xls"> <el-upload drag :before-upload="handleUpload" :show-file-list="false" accept=".xlsx,.xls">
@@ -188,38 +118,28 @@
<script> <script>
import { import {
getGoodsCategoryAll, getGoodsCategoryAll,
getGoodsListDataByStockSeller,
getGoodsSkuListDataSeller, getGoodsSkuListDataSeller,
importStockExcel, importStockExcel,
queryExportStock, queryExportStock,
updateGoodsAlertStocks,
updateGoodsSkuStocks,
} from "@/api/goods"; } from "@/api/goods";
export default { export default {
name: "goods", name: "alertQuantity",
data() { data() {
return { return {
importModal: false, importModal: false,
spinShow: false, spinShow: false,
loading: true, loading: true,
updateStockModalVisible: false,
stockType: "stockManage",
searchForm: { searchForm: {
pageNumber: 1, pageNumber: 1,
pageSize: 20, pageSize: 20,
sort: "create_time", sort: "create_time",
order: "desc", order: "desc",
}, },
checkFlag: false,
stockList: [],
warnData: [],
skuAllData: [],
stockAllData: [], stockAllData: [],
total: 0, total: 0,
categoryList: [], categoryList: [],
category: [], category: [],
selectedSku: {},
file: null, file: null,
}; };
}, },
@@ -228,51 +148,6 @@ export default {
this.getDataList(); this.getDataList();
this.deepGroup(); this.deepGroup();
}, },
openUpdataStockModal(row) {
this.stockList = [];
this.selectedSku = JSON.parse(JSON.stringify(row));
this.stockList.push(this.selectedSku);
this.updateStockModalVisible = true;
},
updateStock() {
const updateStockList = this.stockList.map((i) => ({
skuId: i.id,
quantity: i.quantity,
}));
updateGoodsSkuStocks(updateStockList).then((res) => {
if (res.success) {
this.updateStockModalVisible = false;
this.$Message.success("更新库存成功");
this.getDataList();
}
});
},
updateWarnStock(row) {
if (this.checkFlag) {
updateGoodsAlertStocks({ skuId: row.id, alertQuantity: row.alertQuantity }).then((res) => {
if (res.success) {
this.$Message.success("更新成功");
}
});
}
},
checkVal(row) {
if (
!/^[+]{0,1}(\d+)$|^[+]{0,1}(\d+\.\d+)$/.test(row.alertQuantity) ||
parseInt(row.alertQuantity) < 0 ||
parseInt(row.alertQuantity) > 99999999
) {
this.$Message.error("请输入0~99999999之间的数字值");
row.alertQuantity = 0;
this.checkFlag = false;
return;
}
this.checkFlag = true;
},
switchTabs() {
this.handleReset();
this.getDataList();
},
changePageSize(v) { changePageSize(v) {
this.searchForm.pageSize = v; this.searchForm.pageSize = v;
this.getDataList(); this.getDataList();
@@ -298,23 +173,6 @@ export default {
}, },
getDataList() { getDataList() {
this.loading = true; this.loading = true;
if (this.stockType === "warnList") {
getGoodsListDataByStockSeller(this.searchForm).then((res) => {
if (res.success) {
this.warnData = res.result.records;
this.total = res.result.total;
}
this.loading = false;
});
} else if (this.stockType === "warnSetting") {
getGoodsSkuListDataSeller(this.searchForm).then((res) => {
if (res.success) {
this.skuAllData = res.result.records;
this.total = res.result.total;
}
this.loading = false;
});
} else {
getGoodsSkuListDataSeller(this.searchForm).then((res) => { getGoodsSkuListDataSeller(this.searchForm).then((res) => {
if (res.success) { if (res.success) {
this.stockAllData = res.result.records; this.stockAllData = res.result.records;
@@ -322,7 +180,6 @@ export default {
} }
this.loading = false; this.loading = false;
}); });
}
}, },
deepGroup() { deepGroup() {
getGoodsCategoryAll().then((res) => { getGoodsCategoryAll().then((res) => {
@@ -394,9 +251,4 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "@/styles/table-common.scss"; @import "@/styles/table-common.scss";
.link-text {
color: #2d8cf0;
cursor: pointer;
text-decoration: none;
}
</style> </style>

View File

@@ -0,0 +1,295 @@
<template>
<div class="search">
<el-card>
<div @keyup.enter="handleSearch">
<el-form ref="searchForm" :model="searchForm" inline label-width="70px" class="search-form">
<el-form-item label="商品名称" prop="goodsName">
<el-input v-model="searchForm.goodsName" placeholder="请输入商品名称" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="商品编号" prop="goodsId">
<el-input v-model="searchForm.goodsId" placeholder="请输入商品编号" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="searchForm.marketEnable" placeholder="请选择" clearable style="width: 200px">
<el-option label="下架" value="DOWN" />
<el-option label="上架" value="UPPER" />
</el-select>
</el-form-item>
<el-form-item label="商品分类" prop="category">
<el-cascader
v-model="category"
:options="categoryList"
placeholder="请选择商品分类"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="货号" prop="sn">
<el-input v-model="searchForm.sn" placeholder="请输入货号" clearable style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
<el-button class="search-btn" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-tabs v-model="activeTab" @tab-click="switchTabs">
<el-tab-pane label="预警商品" name="warnList">
<el-table v-loading="loading" border :data="warnData" class="mt_10" style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" min-width="400" show-overflow-tooltip />
<el-table-column label="库存" min-width="120">
<template #default="{ row }">{{ row.quantity || 0 }}</template>
</el-table-column>
<el-table-column label="预警值" min-width="120">
<template #default="{ row }">{{ row.alertQuantity || 0 }}</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row }">
<a class="link-text" @click="openUpdataStockModal(row)">库存</a>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="设置预警" name="warnSetting">
<el-table v-loading="loading" border :data="skuAllData" class="mt_10" style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" />
<el-table-column label="库存" width="200">
<template #default="{ row }">{{ row.quantity || 0 }}</template>
</el-table-column>
<el-table-column label="预警值" width="200">
<template #default="{ row }">
<el-input
v-model="row.alertQuantity"
type="number"
clearable
placeholder="请输入预警库存"
@blur="updateWarnStock(row)"
@change="checkVal(row)"
/>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:page-sizes="[20, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="changePage"
@size-change="changePageSize"
/>
</div>
</el-card>
<el-dialog v-model="updateStockModalVisible" title="更新库存" width="610px" :close-on-click-modal="false">
<el-table :data="stockList" border class="mt_10" style="width: 100%">
<el-table-column label="sku规格" min-width="120">
<template #default="{ row }">{{ row.simpleSpecs }}</template>
</el-table-column>
<el-table-column label="审核状态" width="130">
<template #default="{ row }">
<el-tag
:type="
row.authFlag === 'PASS' ? 'success' : row.authFlag === 'TOBEAUDITED' ? 'primary' : 'danger'
"
>
{{
row.authFlag === "TOBEAUDITED" ? "待审核" : row.authFlag === "PASS" ? "通过" : "审核拒绝"
}}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center">
<template #default="{ row }">
<el-input-number v-model="row.quantity" :min="0" />
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="updateStockModalVisible = false">取消</el-button>
<el-button type="primary" @click="updateStock">更新</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import {
getGoodsCategoryAll,
getGoodsListDataByStockSeller,
getGoodsSkuListDataSeller,
updateGoodsAlertStocks,
updateGoodsSkuStocks,
} from "@/api/goods";
const ROUTE_TAB_MAP = {
alertQuantityWarn: "warnList",
alertQuantityWarnList: "warnList",
alertQuantityWarnSetting: "warnSetting",
};
export default {
name: "alertQuantityWarn",
data() {
return {
loading: true,
updateStockModalVisible: false,
activeTab: "warnList",
searchForm: {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
},
checkFlag: false,
stockList: [],
warnData: [],
skuAllData: [],
total: 0,
categoryList: [],
category: [],
selectedSku: {},
};
},
watch: {
"$route.name"(name) {
this.applyRouteTab(name);
this.handleReset();
},
},
mounted() {
this.applyRouteTab();
this.init();
},
methods: {
applyRouteTab(name = this.$route.name) {
this.activeTab = ROUTE_TAB_MAP[name] || "warnList";
},
init() {
this.getDataList();
this.deepGroup();
},
openUpdataStockModal(row) {
this.stockList = [];
this.selectedSku = JSON.parse(JSON.stringify(row));
this.stockList.push(this.selectedSku);
this.updateStockModalVisible = true;
},
updateStock() {
const updateStockList = this.stockList.map((i) => ({
skuId: i.id,
quantity: i.quantity,
}));
updateGoodsSkuStocks(updateStockList).then((res) => {
if (res.success) {
this.updateStockModalVisible = false;
this.$Message.success("更新库存成功");
this.getDataList();
}
});
},
updateWarnStock(row) {
if (this.checkFlag) {
updateGoodsAlertStocks({ skuId: row.id, alertQuantity: row.alertQuantity }).then((res) => {
if (res.success) {
this.$Message.success("更新成功");
}
});
}
},
checkVal(row) {
if (
!/^[+]{0,1}(\d+)$|^[+]{0,1}(\d+\.\d+)$/.test(row.alertQuantity) ||
parseInt(row.alertQuantity) < 0 ||
parseInt(row.alertQuantity) > 99999999
) {
this.$Message.error("请输入0~99999999之间的数字值");
row.alertQuantity = 0;
this.checkFlag = false;
return;
}
this.checkFlag = true;
},
switchTabs() {
this.handleReset();
},
changePageSize(v) {
this.searchForm.pageSize = v;
this.getDataList();
},
changePage(v) {
this.searchForm.pageNumber = v;
this.getDataList();
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.searchForm.categoryPath = this.category?.length ? this.category.join(",") : null;
this.getDataList();
},
handleReset() {
this.searchForm = {
pageNumber: 1,
pageSize: 20,
sort: "create_time",
order: "desc",
};
this.category = [];
this.getDataList();
},
getDataList() {
this.loading = true;
if (this.activeTab === "warnList") {
getGoodsListDataByStockSeller(this.searchForm).then((res) => {
if (res.success) {
this.warnData = res.result.records;
this.total = res.result.total;
}
this.loading = false;
});
} else {
getGoodsSkuListDataSeller(this.searchForm).then((res) => {
if (res.success) {
this.skuAllData = res.result.records;
this.total = res.result.total;
}
this.loading = false;
});
}
},
deepGroup() {
getGoodsCategoryAll().then((res) => {
if (res.success) {
this.categoryList = res.result.map((item) => ({
value: item.id,
label: item.name,
children: (item.children || []).map((child) => ({
value: child.id,
label: child.name,
children: (child.children || []).map((grandson) => ({
value: grandson.id,
label: grandson.name,
})),
})),
}));
}
});
},
},
};
</script>
<style lang="scss" scoped>
@import "@/styles/table-common.scss";
.link-text {
color: #2d8cf0;
cursor: pointer;
text-decoration: none;
}
</style>

View File

@@ -226,7 +226,14 @@ export default {
setData(v, init) { setData(v, init) {
if (typeof v == "string") { if (typeof v == "string") {
if (this.multiple) { if (this.multiple) {
this.$Message.warning("多张上传仅支持数组数据类型"); if (!v) {
this.uploadList = [];
return;
}
this.setData(
v.split(",").map((s) => s.trim()).filter(Boolean),
init
);
return; return;
} }
if (!v) { if (!v) {

View File

@@ -0,0 +1,81 @@
export const PROCUREMENT_STATUS = {
DRAFT: "待提交",
SUBMITTED: "已提交",
PENDING_INBOUND: "待入库",
PARTIAL_INBOUND: "部分入库",
CLOSED: "已关闭",
COMPLETED: "已完成",
REJECTED: "已拒绝",
};
export const PROCUREMENT_STATUS_TAG = {
DRAFT: "info",
SUBMITTED: "warning",
PENDING_INBOUND: "primary",
PARTIAL_INBOUND: "warning",
CLOSED: "info",
COMPLETED: "success",
REJECTED: "danger",
};
export const DAMAGE_STATUS = {
DRAFT: "待提交",
SUBMITTED: "待审核",
APPROVED: "已通过",
REJECTED: "已驳回",
CANCELLED: "已作废",
COMPLETED: "已完成",
};
export const DAMAGE_STATUS_TAG = {
DRAFT: "info",
SUBMITTED: "warning",
APPROVED: "primary",
REJECTED: "danger",
CANCELLED: "info",
COMPLETED: "success",
};
export const MARKET_ENABLE = {
UPPER: "上架",
DOWN: "下架",
};
export function procurementStatusText(status) {
return PROCUREMENT_STATUS[status] || status || "-";
}
export function procurementStatusTag(status) {
return PROCUREMENT_STATUS_TAG[status] || "info";
}
export function damageStatusText(status) {
return DAMAGE_STATUS[status] || status || "-";
}
export function damageStatusTag(status) {
return DAMAGE_STATUS_TAG[status] || "info";
}
export function formatMoney(val) {
if (val === null || val === undefined || val === "") return "0.00";
return Number(val).toFixed(2);
}
export function exportCsv(filename, headers, rows) {
const escape = (v) => {
const s = v == null ? "" : String(v);
if (s.includes(",") || s.includes('"') || s.includes("\n")) {
return `"${s.replace(/"/g, '""')}"`;
}
return s;
};
const lines = [headers.map(escape).join(",")];
rows.forEach((row) => lines.push(row.map(escape).join(",")));
const blob = new Blob(["\uFEFF" + lines.join("\n")], { type: "text/csv;charset=utf-8;" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
}

View File

@@ -0,0 +1,303 @@
<template>
<div class="search">
<el-card>
<el-form ref="searchForm" :model="searchForm" inline label-width="80px">
<el-form-item label="创建时间">
<el-date-picker
v-model="dateRange"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
start-placeholder="开始"
end-placeholder="结束"
style="width: 360px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="mt_10">
<div class="order-tab">
<el-tabs v-model="currentStatus" @tab-click="onStatusTabClick">
<el-tab-pane
v-for="item in statusTabs"
:key="item.value"
:label="item.title"
:name="item.value"
/>
</el-tabs>
</div>
<div class="operation padding-row">
<el-button type="primary" @click="goCreate">新建报损单</el-button>
</div>
<el-table v-loading="loading" :data="data" border class="mt_10" style="width: 100%">
<el-table-column prop="sn" label="报损单号" width="220" show-overflow-tooltip />
<el-table-column prop="totalQuantity" label="报损数量" width="90" align="center" />
<el-table-column label="报损金额" width="110" align="right">
<template #default="{ row }">{{ formatMoney(row.totalAmount) }}</template>
</el-table-column>
<el-table-column prop="damageDate" label="报损日期" width="120" />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="damageStatusTag(row.status)">{{ damageStatusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="260" align="center" fixed="right">
<template #default="{ row }">
<a class="link-text" @click="goDetail(row)">查看</a>
<template v-if="row.status === 'DRAFT'">
<span class="op-split">|</span>
<a class="link-text" @click="handleSubmit(row)">提交</a>
</template>
<template v-if="row.status === 'SUBMITTED'">
<span class="op-split">|</span>
<a class="link-text" @click="handleApprove(row)">通过</a>
<span class="op-split">|</span>
<a class="link-text" @click="handleReject(row)">驳回</a>
</template>
<template v-if="row.status === 'APPROVED'">
<span class="op-split">|</span>
<a class="link-text" @click="handleComplete(row)">完成报损</a>
</template>
<template v-if="['DRAFT', 'SUBMITTED', 'APPROVED', 'REJECTED'].includes(row.status)">
<span class="op-split">|</span>
<a class="link-text" @click="handleCancel(row)">作废</a>
</template>
</template>
</el-table-column>
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="getDataList"
@size-change="getDataList"
/>
</div>
</el-card>
</div>
</template>
<script>
import { ElMessageBox } from "element-plus";
import {
getDamageReportPage,
getDamageReportStatusCount,
submitDamageReport,
approveDamageReport,
rejectDamageReport,
cancelDamageReport,
completeDamageReport,
} from "@/api/procurement";
import {
DAMAGE_STATUS,
damageStatusText,
damageStatusTag,
formatMoney,
} from "../constants";
export default {
name: "procurement-damage-report-list",
data() {
return {
loading: false,
data: [],
total: 0,
dateRange: [],
currentStatus: "ALL",
statusCountData: {},
searchForm: {
pageNumber: 1,
pageSize: 10,
status: "",
},
};
},
computed: {
statusTabs() {
const tabs = [{ title: "全部", value: "ALL" }];
Object.keys(DAMAGE_STATUS).forEach((key) => {
const count = this.statusCountData[key];
const label = DAMAGE_STATUS[key];
tabs.push({
title: count ? `${label}${count}` : label,
value: key,
});
});
return tabs;
},
},
mounted() {
this.init();
},
methods: {
damageStatusText,
damageStatusTag,
formatMoney,
init() {
this.getStatusCount();
this.getDataList();
},
buildSearchParams(includeStatus = true) {
const params = { ...this.searchForm };
if (!includeStatus) {
delete params.status;
}
if (this.dateRange && this.dateRange.length === 2) {
params.startDate = this.dateRange[0];
params.endDate = this.dateRange[1];
}
return params;
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getStatusCount();
this.getDataList();
},
resetSearch() {
this.searchForm = { pageNumber: 1, pageSize: 10, status: "" };
this.dateRange = [];
this.currentStatus = "ALL";
this.getStatusCount();
this.getDataList();
},
onStatusTabClick(tab) {
this.statusClick(tab.paneName);
},
statusClick(name) {
if (name === "ALL" || name === "" || name === undefined) {
this.searchForm.status = "";
this.currentStatus = "ALL";
} else {
this.searchForm.status = name;
this.currentStatus = name;
}
this.searchForm.pageNumber = 1;
this.getDataList();
},
getStatusCount() {
getDamageReportStatusCount(this.buildSearchParams(false)).then((res) => {
if (res.success) {
this.statusCountData = res.result || {};
}
});
},
getDataList() {
this.loading = true;
getDamageReportPage(this.buildSearchParams(true))
.then((res) => {
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
refreshList() {
this.getStatusCount();
this.getDataList();
},
goCreate() {
this.$filters.customRouterPush({ name: "procurement-damage-report-operation" });
},
goDetail(row) {
this.$filters.customRouterPush({
name: "procurement-damage-report-operation",
query: { id: row.id, readonly: 1 },
});
},
handleSubmit(row) {
this.$Modal.confirm({
title: "提交报损单",
content: "确认提交该报损单?",
onOk: () => {
submitDamageReport(row.id).then((res) => {
if (res.success) {
this.$Message.success("提交成功");
this.refreshList();
}
});
},
});
},
handleApprove(row) {
this.$Modal.confirm({
title: "审核通过",
content: "确认通过该报损单?",
onOk: () => {
approveDamageReport(row.id).then((res) => {
if (res.success) {
this.$Message.success("审核通过");
this.refreshList();
}
});
},
});
},
handleReject(row) {
ElMessageBox.prompt("请输入驳回原因", "驳回报损单", {
confirmButtonText: "确定",
cancelButtonText: "取消",
inputPlaceholder: "驳回原因",
})
.then(({ value }) => {
rejectDamageReport(row.id, value || "驳回").then((res) => {
if (res.success) {
this.$Message.success("已驳回");
this.refreshList();
}
});
})
.catch(() => {});
},
handleCancel(row) {
this.$Modal.confirm({
title: "作废报损单",
content: "确认作废该报损单?",
onOk: () => {
cancelDamageReport(row.id).then((res) => {
if (res.success) {
this.$Message.success("已作废");
this.refreshList();
}
});
},
});
},
handleComplete(row) {
this.$Modal.confirm({
title: "完成报损",
content: "完成后将扣减库存,是否继续?",
onOk: () => {
completeDamageReport(row.id).then((res) => {
if (res.success) {
this.$Message.success("报损完成,库存已扣减");
this.refreshList();
}
});
},
});
},
},
};
</script>
<style scoped>
.op-split {
margin: 0 6px;
color: #dcdfe6;
}
.order-tab :deep(.el-tabs__item) {
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,378 @@
<template>
<div class="search">
<el-card>
<template #header>
<div class="card-header">
<span>{{ pageTitle }}</span>
<el-button @click="goBack">返回列表</el-button>
</div>
</template>
<template v-if="readonly">
<el-descriptions :column="2" border>
<el-descriptions-item label="报损单号">{{ detail.sn || "-" }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="damageStatusTag(detail.status)">{{ damageStatusText(detail.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建人">{{ detail.makerName || detail.createBy || "-" }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detail.createTime || "-" }}</el-descriptions-item>
<el-descriptions-item label="审核人">{{ detail.auditorName || "-" }}</el-descriptions-item>
<el-descriptions-item label="审核时间">{{ detail.auditTime || "-" }}</el-descriptions-item>
<el-descriptions-item label="报损日期">{{ detail.damageDate || "-" }}</el-descriptions-item>
<el-descriptions-item label="报损原因">{{ reasonText }}</el-descriptions-item>
<el-descriptions-item label="报损数量">{{ detail.totalQuantity ?? "-" }}</el-descriptions-item>
<el-descriptions-item label="报损金额">{{ formatMoney(detail.totalAmount) }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detail.remark || "-" }}</el-descriptions-item>
</el-descriptions>
<el-form label-width="100px" class="mt_10">
<el-form-item label="凭证图片">
<upload-pic-thumb
v-if="evidenceImageList.length"
v-model="evidenceImageList"
:limit="5"
:disable="true"
:remove="false"
:is-view="true"
/>
<span v-else class="empty-text">暂无凭证</span>
</el-form-item>
</el-form>
<el-table :data="items" border class="mt_10 item-table" style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" class-name="goods-name-col" width="200">
<template #default="{ row }">{{ row.goodsName }}</template>
</el-table-column>
<el-table-column prop="skuId" label="SKU ID" class-name="sku-id-col" width="200">
<template #default="{ row }">{{ row.skuId }}</template>
</el-table-column>
<el-table-column prop="quantity" label="报损数量" width="100" align="center" />
<el-table-column label="单价" width="110" align="right">
<template #default="{ row }">{{ formatMoney(row.unitPrice) }}</template>
</el-table-column>
<el-table-column label="金额" width="100" align="right">
<template #default="{ row }">{{ formatMoney(row.amount) }}</template>
</el-table-column>
</el-table>
</template>
<template v-else>
<el-form :model="form" label-width="100px">
<el-form-item label="报损日期">
<el-date-picker
v-model="form.damageDate"
type="date"
value-format="YYYY-MM-DD"
:disabled="readonly"
placeholder="报损日期"
style="width: 200px"
/>
</el-form-item>
<el-form-item label="报损原因">
<el-select
v-model="form.damageReasonId"
:disabled="readonly"
placeholder="请选择报损原因"
style="width: 300px"
>
<el-option
v-for="item in reasonOptions"
:key="item.id"
:label="item.reason"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="form.remark"
type="textarea"
:rows="2"
:disabled="readonly"
style="max-width: 600px"
/>
</el-form-item>
<el-form-item label="凭证图片">
<upload-pic-thumb v-model="form.evidence" :limit="5" :disable="readonly" :is-view="readonly" />
</el-form-item>
</el-form>
<div v-if="!readonly" class="operation padding-row">
<el-button type="primary" @click="openSkuPicker">添加商品</el-button>
</div>
<el-table :data="items" border class="mt_10 item-table" style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" class-name="goods-name-col" width="200">
<template #default="{ row }">{{ row.goodsName }}</template>
</el-table-column>
<el-table-column prop="skuId" label="SKU ID" class-name="sku-id-col" width="200">
<template #default="{ row }">{{ row.skuId }}</template>
</el-table-column> <el-table-column label="报损数量" width="100" align="center">
<template #default="{ row }">
<el-input
v-if="!readonly"
v-model="row.quantity"
class="cell-input"
@blur="onQuantityBlur(row)"
/>
<span v-else>{{ row.quantity }}</span>
</template>
</el-table-column>
<el-table-column label="单价" width="110" align="center">
<template #default="{ row }">
<el-input
v-if="!readonly"
v-model="row.unitPrice"
class="cell-input"
@blur="onUnitPriceBlur(row)"
/>
<span v-else>{{ formatMoney(row.unitPrice) }}</span>
</template>
</el-table-column>
<el-table-column label="金额" width="100" align="right">
<template #default="{ row }">{{ formatMoney(row.amount) }}</template>
</el-table-column>
<el-table-column v-if="!readonly" label="操作" width="70" align="center" fixed="right">
<template #default="{ $index }">
<a class="link-text" @click="removeItem($index)">删除</a>
</template>
</el-table-column>
</el-table>
<div class="summary mt_10">
<span>合计数量<b>{{ totalQuantity }}</b></span>
<span class="ml_20">合计金额<b>{{ formatMoney(totalAmount) }}</b></span>
</div>
<div class="footer-actions mt_20">
<el-button type="primary" :loading="submitLoading" @click="handleSave(false)">保存草稿</el-button>
<el-button type="success" :loading="submitLoading" @click="handleSave(true)">保存并提交</el-button>
</div>
</template>
</el-card>
<liliDialog ref="liliDialog" @selectedGoodsData="onGoodsSelected" />
</div>
</template>
<script>
import liliDialog from "@/views/lili-dialog";
import uploadPicThumb from "@/views/my-components/lili/upload-pic-thumb";
import {
createDamageReport,
getDamageReportDetail,
getDamageReportItems,
getStockReasonList,
submitDamageReport,
} from "@/api/procurement";
import { damageStatusText, damageStatusTag, formatMoney } from "../constants";
export default {
name: "procurement-damage-report-operation",
components: { liliDialog, uploadPicThumb },
data() {
return {
readonly: false,
submitLoading: false,
form: {
damageDate: "",
damageReasonId: "",
remark: "",
evidence: [],
},
items: [],
detail: {},
reasonOptions: [],
evidenceImageList: [],
};
},
computed: {
pageTitle() {
return this.readonly ? "报损单详情" : "新建报损单";
},
reasonText() {
const reason = this.reasonOptions.find((item) => item.id === this.detail.damageReasonId);
return reason ? reason.reason : "-";
},
totalQuantity() {
return this.items.reduce((sum, item) => sum + (Number(item.quantity) || 0), 0);
},
totalAmount() {
return this.items.reduce((sum, item) => sum + (Number(item.amount) || 0), 0);
},
},
mounted() {
this.readonly = this.$route.query.readonly === "1" || !!this.$route.query.id;
this.loadReasons();
if (this.$route.query.id) {
this.loadDetail(this.$route.query.id);
}
},
methods: {
damageStatusText,
damageStatusTag,
formatMoney,
loadReasons() {
getStockReasonList({ category: "DAMAGE", pageNumber: 1, pageSize: 100 }).then((res) => {
if (res.success) {
this.reasonOptions = res.result.records || [];
}
});
},
recalcRow(row) {
row.amount = (Number(row.unitPrice) || 0) * (Number(row.quantity) || 0);
},
onQuantityBlur(row) {
const val = parseInt(row.quantity, 10);
row.quantity = Number.isNaN(val) || val < 1 ? 1 : val;
this.recalcRow(row);
},
onUnitPriceBlur(row) {
const val = parseFloat(row.unitPrice);
row.unitPrice = Number.isNaN(val) || val < 0 ? 0 : Math.round(val * 100) / 100;
this.recalcRow(row);
},
loadDetail(id) {
getDamageReportDetail(id).then((res) => {
if (res.success && res.result) {
this.detail = res.result;
this.form.damageDate = res.result.damageDate;
this.form.damageReasonId = res.result.damageReasonId;
this.form.remark = res.result.remark;
this.form.evidence = res.result.evidence;
this.evidenceImageList = this.parseEvidenceImages(res.result.evidence);
}
});
getDamageReportItems(id).then((res) => {
if (res.success) {
this.items = res.result || [];
}
});
},
parseEvidenceImages(val) {
if (!val) return [];
if (Array.isArray(val)) return val.filter(Boolean);
return String(val)
.split(",")
.map((s) => s.trim())
.filter(Boolean);
},
openSkuPicker() {
this.$refs.liliDialog.goodsData = this.items.map((item) => ({
id: item.skuId,
goodsId: item.goodsId,
goodsName: item.goodsName,
price: item.unitPrice,
cost: item.unitPrice,
}));
this.$refs.liliDialog.open("goods");
},
onGoodsSelected(selected) {
const map = new Map(this.items.map((item) => [item.skuId, item]));
this.items = selected.map((sku) => {
const existed = map.get(sku.id);
if (existed) return existed;
const unitPrice = sku.cost || sku.price || 0;
return {
goodsId: sku.goodsId,
skuId: sku.id,
goodsName: sku.goodsName,
quantity: 1,
unitPrice,
amount: unitPrice,
};
});
},
removeItem(index) {
this.items.splice(index, 1);
},
validateForm() {
if (!this.form.damageReasonId) {
this.$Message.warning("请选择报损原因");
return false;
}
if (!this.items.length) {
this.$Message.warning("请添加报损商品");
return false;
}
return true;
},
handleSave(andSubmit) {
if (!this.validateForm()) return;
this.submitLoading = true;
const payload = {
damageDate: this.form.damageDate || undefined,
damageReasonId: this.form.damageReasonId,
remark: this.form.remark,
evidence: Array.isArray(this.form.evidence)
? this.form.evidence.join(",")
: this.form.evidence || "",
items: this.items.map((item) => ({
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
quantity: item.quantity,
unitPrice: item.unitPrice,
})),
};
createDamageReport(payload)
.then((res) => {
if (!res.success) return;
const reportId = res.result.id;
if (andSubmit) {
return submitDamageReport(reportId).then((subRes) => {
if (subRes.success) {
this.$Message.success("保存并提交成功");
this.goBack();
}
});
}
this.$Message.success("保存成功");
this.goBack();
})
.finally(() => {
this.submitLoading = false;
});
},
goBack() {
this.$filters.customRouterPush({ name: "procurementDamageReportList" });
},
},
};
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.summary {
padding: 12px 0;
}
.ml_20 {
margin-left: 20px;
}
.footer-actions {
display: flex;
gap: 12px;
}
.item-table :deep(.cell-input) {
width: 100%;
}
.item-table :deep(.cell-input .el-input__wrapper) {
padding-left: 8px;
padding-right: 8px;
}
.item-table :deep(.sku-id-col .cell) {
white-space: normal;
word-break: break-all;
line-height: 1.4;
}
.item-table :deep(.goods-name-col .cell) {
white-space: normal;
word-break: break-all;
line-height: 1.4;
}
.empty-text {
color: #909399;
}
</style>

View File

@@ -0,0 +1,109 @@
<template>
<div class="search">
<el-card>
<el-form ref="searchForm" :model="searchForm" inline label-width="80px">
<el-form-item label="入库单号">
<el-input v-model="searchForm.inboundSn" placeholder="入库单号" clearable style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="mt_10">
<div class="operation padding-row">
<el-button type="primary" @click="goCreate">新建入库单</el-button>
</div>
<el-table v-loading="loading" :data="data" border class="mt_10" style="width: 100%">
<el-table-column prop="inboundSn" label="入库单号" width="220" show-overflow-tooltip />
<el-table-column prop="procurementOrderId" label="采购单ID" width="180" show-overflow-tooltip />
<el-table-column prop="confirmedQuantity" label="已入库量" width="90" align="center" />
<el-table-column prop="pendingQuantity" label="待入库量" width="90" align="center" />
<el-table-column label="入库成本" width="110" align="right">
<template #default="{ row }">{{ formatMoney(row.totalCost) }}</template>
</el-table-column>
<el-table-column prop="operatorName" label="操作人" width="120" show-overflow-tooltip />
<el-table-column prop="inboundTime" label="入库时间" width="170" />
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="{ row }">
<a class="link-text" @click="goDetail(row)">查看</a>
</template>
</el-table-column>
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="getDataList"
@size-change="getDataList"
/>
</div>
</el-card>
</div>
</template>
<script>
import { getProcurementInboundPage } from "@/api/procurement";
import { formatMoney } from "../constants";
export default {
name: "procurement-inbound-list",
data() {
return {
loading: false,
data: [],
total: 0,
searchForm: {
pageNumber: 1,
pageSize: 10,
inboundSn: "",
sort: "createTime",
order: "desc",
},
};
},
mounted() {
this.getDataList();
},
methods: {
formatMoney,
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
resetSearch() {
this.searchForm.inboundSn = "";
this.searchForm.pageNumber = 1;
this.getDataList();
},
getDataList() {
this.loading = true;
getProcurementInboundPage(this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
goCreate() {
this.$filters.customRouterPush({ name: "procurement-inbound-operation" });
},
goDetail(row) {
this.$filters.customRouterPush({
name: "procurement-inbound-operation",
query: { id: row.id, readonly: 1 },
});
},
},
};
</script>

View File

@@ -0,0 +1,294 @@
<template>
<div class="search">
<el-card>
<template #header>
<div class="card-header">
<span>{{ pageTitle }}</span>
<el-button @click="goBack">返回列表</el-button>
</div>
</template>
<template v-if="!readonly">
<el-form label-width="100px">
<el-form-item label="采购单" required>
<el-select
v-model="form.procurementOrderId"
filterable
placeholder="请选择待入库采购单"
style="width: 400px"
@change="onOrderChange"
>
<el-option
v-for="item in orderOptions"
:key="item.id"
:label="`${item.orderSn}${procurementStatusText(item.status)}`"
:value="item.id"
/>
</el-select>
<el-button class="ml_10" @click="loadPendingOrders">刷新</el-button>
</el-form-item>
<el-form-item label="入库凭证">
<upload-pic-thumb v-model="form.certificateImages" :limit="5" />
</el-form-item>
</el-form>
<el-table :data="items" border class="item-table" style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" class-name="goods-name-col" width="200">
<template #default="{ row }">{{ row.goodsName }}</template>
</el-table-column>
<el-table-column prop="skuId" label="SKU ID" class-name="sku-id-col" width="200">
<template #default="{ row }">{{ row.skuId }}</template>
</el-table-column>
<el-table-column label="采购数量" width="90" align="center">
<template #default="{ row }">{{ row.quantity }}</template>
</el-table-column>
<el-table-column label="已入库" width="80" align="center">
<template #default="{ row }">{{ row.receivedQuantity || 0 }}</template>
</el-table-column>
<el-table-column label="剩余可入" width="90" align="center">
<template #default="{ row }">{{ remainQty(row) }}</template>
</el-table-column>
<el-table-column label="本次入库" width="110" align="center">
<template #default="{ row }">
<el-input
v-model="row.inboundQuantity"
class="cell-input"
@blur="onInboundQtyBlur(row)"
/>
</template>
</el-table-column>
</el-table>
<div class="footer-actions mt_20">
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确认入库</el-button>
</div>
</template>
<template v-else>
<el-descriptions :column="2" border>
<el-descriptions-item label="入库单号">{{ detail.inboundSn }}</el-descriptions-item>
<el-descriptions-item label="采购单ID">{{ detail.procurementOrderId }}</el-descriptions-item>
<el-descriptions-item label="本次入库数量">{{ totalInboundQuantity }}</el-descriptions-item>
<el-descriptions-item label="入库成本">{{ formatMoney(detail.totalCost) }}</el-descriptions-item>
<el-descriptions-item label="操作人">{{ detail.operatorName }}</el-descriptions-item>
<el-descriptions-item label="入库时间">{{ detail.inboundTime }}</el-descriptions-item>
</el-descriptions>
<el-form label-width="100px" class="mt_10">
<el-form-item label="入库凭证">
<upload-pic-thumb
v-if="certificateImageList.length"
v-model="certificateImageList"
:limit="5"
:disable="true"
:remove="false"
:is-view="true"
/>
<span v-else class="empty-text">暂无凭证</span>
</el-form-item>
</el-form>
<el-table :data="inboundItems" border class="mt_10 item-table" style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" class-name="goods-name-col" width="200">
<template #default="{ row }">{{ row.goodsName }}</template>
</el-table-column>
<el-table-column prop="skuId" label="SKU ID" class-name="sku-id-col" width="200">
<template #default="{ row }">{{ row.skuId }}</template>
</el-table-column>
<el-table-column prop="inboundQuantity" label="本次入库" width="90" align="center" />
</el-table>
</template>
</el-card>
</div>
</template>
<script>
import uploadPicThumb from "@/views/my-components/lili/upload-pic-thumb";
import {
getProcurementOrderPage,
getProcurementOrderDetail,
createProcurementInbound,
getProcurementInboundDetail,
getProcurementInboundItems,
} from "@/api/procurement";
import { procurementStatusText, formatMoney } from "../constants";
export default {
name: "procurement-inbound-operation",
components: { uploadPicThumb },
data() {
return {
readonly: false,
submitLoading: false,
form: {
procurementOrderId: "",
certificateImages: [],
},
orderOptions: [],
items: [],
detail: {},
inboundItems: [],
certificateImageList: [],
};
},
computed: {
pageTitle() {
return this.readonly ? "入库单详情" : "新建入库单";
},
totalInboundQuantity() {
return (this.inboundItems || []).reduce(
(sum, item) => sum + (Number(item.inboundQuantity) || 0),
0
);
},
},
mounted() {
this.readonly = this.$route.query.readonly === "1";
if (this.$route.query.id) {
this.loadInboundDetail(this.$route.query.id);
} else {
this.loadPendingOrders();
if (this.$route.query.procurementOrderId) {
this.form.procurementOrderId = this.$route.query.procurementOrderId;
this.onOrderChange(this.$route.query.procurementOrderId);
}
}
},
methods: {
procurementStatusText,
formatMoney,
remainQty(row) {
return (row.quantity || 0) - (row.receivedQuantity || 0);
},
onInboundQtyBlur(row) {
const max = this.remainQty(row);
const val = parseInt(row.inboundQuantity, 10);
if (Number.isNaN(val) || val < 0) {
row.inboundQuantity = 0;
} else if (val > max) {
row.inboundQuantity = max;
} else {
row.inboundQuantity = val;
}
},
loadPendingOrders() {
getProcurementOrderPage({
pageNumber: 1,
pageSize: 100,
status: "",
}).then((res) => {
if (res.success) {
this.orderOptions = (res.result.records || []).filter((o) =>
["PENDING_INBOUND", "PARTIAL_INBOUND"].includes(o.status)
);
}
});
},
onOrderChange(orderId) {
if (!orderId) {
this.items = [];
return;
}
getProcurementOrderDetail(orderId).then((res) => {
if (res.success && res.result) {
this.items = (res.result.items || [])
.filter((item) => this.remainQty(item) > 0)
.map((item) => ({
...item,
procurementOrderItemId: item.id,
inboundQuantity: this.remainQty(item),
}));
}
});
},
loadInboundDetail(id) {
getProcurementInboundDetail(id).then((res) => {
if (res.success) {
this.detail = res.result || {};
this.certificateImageList = this.parseCertificateImages(this.detail.certificateImages);
}
});
getProcurementInboundItems(id).then((res) => {
if (res.success) this.inboundItems = res.result || [];
});
},
parseCertificateImages(val) {
if (!val) return [];
if (Array.isArray(val)) return val.filter(Boolean);
return String(val)
.split(",")
.map((s) => s.trim())
.filter(Boolean);
},
handleSubmit() {
if (!this.form.procurementOrderId) {
this.$Message.warning("请选择采购单");
return;
}
const validItems = this.items.filter((item) => item.inboundQuantity > 0);
if (!validItems.length) {
this.$Message.warning("请填写本次入库数量");
return;
}
this.submitLoading = true;
createProcurementInbound({
procurementOrderId: this.form.procurementOrderId,
certificateImages: Array.isArray(this.form.certificateImages)
? this.form.certificateImages.join(",")
: this.form.certificateImages || "",
items: validItems.map((item) => ({
procurementOrderItemId: item.procurementOrderItemId || item.id,
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
inboundQuantity: item.inboundQuantity,
})),
})
.then((res) => {
if (res.success) {
this.$Message.success("入库成功");
this.goBack();
}
})
.finally(() => {
this.submitLoading = false;
});
},
goBack() {
this.$filters.customRouterPush({ name: "procurementInboundList" });
},
},
};
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.ml_10 {
margin-left: 10px;
}
.footer-actions {
margin-top: 20px;
}
.item-table :deep(.cell-input) {
width: 100%;
}
.item-table :deep(.cell-input .el-input__wrapper) {
padding-left: 8px;
padding-right: 8px;
}
.item-table :deep(.sku-id-col .cell) {
white-space: normal;
word-break: break-all;
line-height: 1.4;
}
.item-table :deep(.goods-name-col .cell) {
white-space: normal;
word-break: break-all;
line-height: 1.4;
}
.empty-text {
color: #909399;
}
</style>

View File

@@ -0,0 +1,180 @@
<template>
<div class="search">
<el-card class="info-card" shadow="never">
<template #header>
<div class="card-header">
<span class="section-title">盘点单信息</span>
<el-button @click="goBack">返回列表</el-button>
</div>
</template>
<el-descriptions v-if="detail.id" :column="2" border>
<el-descriptions-item label="盘点单号">{{ detail.sn }}</el-descriptions-item>
<el-descriptions-item label="商品总数">{{ detail.itemTotal }}</el-descriptions-item>
<el-descriptions-item label="盘点时间">{{ detail.countTime }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detail.createTime }}</el-descriptions-item>
<el-descriptions-item label="制单人">{{ detail.makerName }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card class="goods-card mt_10" shadow="never">
<template #header>
<div class="card-header">
<span class="section-title">盘点商品明细</span>
<el-button type="primary" :loading="exporting" @click="handleExport">导出 Excel</el-button>
</div>
</template>
<div @keyup.enter="handleSearch">
<el-form ref="searchFormRef" :model="searchForm" inline label-width="80px" class="search-form">
<el-form-item label="商品名称" prop="goodsName">
<el-input
v-model="searchForm.goodsName"
placeholder="请输入商品名称"
clearable
style="width: 220px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
<el-button class="search-btn" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-table v-loading="loading" :data="data" border style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" width="280" show-overflow-tooltip />
<el-table-column prop="skuName" label="规格" width="120" show-overflow-tooltip />
<el-table-column prop="skuId" label="SKU ID" width="180" show-overflow-tooltip />
<el-table-column label="上架状态" width="90" align="center">
<template #default="{ row }">{{ MARKET_ENABLE[row.marketEnable] || row.marketEnable }}</template>
</el-table-column>
<el-table-column prop="quantity" label="库存数量" width="100" align="center" />
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:page-sizes="[20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="getDataList"
@size-change="getDataList"
/>
</div>
</el-card>
</div>
</template>
<script>
import {
getInventoryCountDetail,
getInventoryCountItemsPage,
exportInventoryCountItems,
} from "@/api/procurement";
import { MARKET_ENABLE } from "../constants";
import { downloadBlob } from "@/utils/downloadBlob";
export default {
name: "procurement-inventory-count-detail",
data() {
return {
MARKET_ENABLE,
loading: false,
exporting: false,
detail: {},
data: [],
total: 0,
sn: "",
searchForm: {
pageNumber: 1,
pageSize: 20,
goodsName: "",
},
};
},
mounted() {
this.sn = this.$route.query.sn || "";
const id = this.$route.query.id;
if (id) {
getInventoryCountDetail(id).then((res) => {
if (res.success) {
this.detail = res.result || {};
this.sn = this.detail.sn || this.sn;
}
});
this.getDataList();
}
},
methods: {
getDataList() {
const id = this.$route.query.id;
if (!id) return;
this.loading = true;
getInventoryCountItemsPage(id, this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
handleSearch() {
this.searchForm.pageNumber = 1;
this.getDataList();
},
handleReset() {
this.searchForm = {
pageNumber: 1,
pageSize: 20,
goodsName: "",
};
this.getDataList();
},
handleExport() {
const id = this.$route.query.id;
if (!id) return;
this.exporting = true;
exportInventoryCountItems(id)
.then((blob) => {
downloadBlob(blob, `盘点单_${this.sn || id}.xlsx`);
})
.finally(() => {
this.exporting = false;
});
},
goBack() {
this.$filters.customRouterPush({ name: "procurementInventoryCountList" });
},
},
};
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.info-card :deep(.el-card__header) {
background: #f5f7fa;
border-bottom: 1px solid #ebeef5;
}
.goods-card :deep(.el-card__header) {
background: #fafafa;
border-bottom: 1px solid #ebeef5;
}
.mt_10 {
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,105 @@
<template>
<div class="search">
<el-card>
<div class="operation padding-row">
<el-button type="primary" :loading="creating" @click="handleCreate">生成盘点单</el-button>
<span class="tip">将快照当前店铺全部 SKU 库存不调整实际库存</span>
</div>
<el-table v-loading="loading" :data="data" border class="mt_10" style="width: 100%">
<el-table-column prop="sn" label="盘点单号" width="220" show-overflow-tooltip />
<el-table-column prop="itemTotal" label="商品数" width="90" align="center" />
<el-table-column prop="makerName" label="制单人" width="120" show-overflow-tooltip />
<el-table-column prop="countTime" label="盘点时间" width="170" />
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="{ row }">
<a class="link-text" @click="goDetail(row)">查看明细</a>
</template>
</el-table-column>
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="getDataList"
@size-change="getDataList"
/>
</div>
</el-card>
</div>
</template>
<script>
import { createInventoryCount, getInventoryCountPage } from "@/api/procurement";
export default {
name: "procurement-inventory-count-list",
data() {
return {
loading: false,
creating: false,
data: [],
total: 0,
searchForm: {
pageNumber: 1,
pageSize: 10,
},
};
},
mounted() {
this.getDataList();
},
methods: {
getDataList() {
this.loading = true;
getInventoryCountPage(this.searchForm)
.then((res) => {
if (res.success) {
this.data = res.result.records || [];
this.total = res.result.total || 0;
}
})
.finally(() => {
this.loading = false;
});
},
handleCreate() {
this.$Modal.confirm({
title: "生成盘点单",
content: "将快照当前店铺全部 SKU 库存,是否继续?",
onOk: () => {
this.creating = true;
createInventoryCount()
.then((res) => {
if (res.success) {
this.$Message.success("盘点单已生成");
this.getDataList();
}
})
.finally(() => {
this.creating = false;
});
},
});
},
goDetail(row) {
this.$filters.customRouterPush({
name: "procurement-inventory-count-detail",
query: { id: row.id, sn: row.sn },
});
},
},
};
</script>
<style scoped>
.tip {
margin-left: 12px;
color: #909399;
font-size: 13px;
}
</style>

View File

@@ -0,0 +1,297 @@
<template>
<div class="search">
<el-card>
<el-form ref="searchForm" :model="searchForm" inline label-width="80px" class="search-form">
<el-form-item label="单据编号">
<el-input v-model="searchForm.orderSn" placeholder="采购单号" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="商品名称">
<el-input v-model="searchForm.goodsName" placeholder="商品名称" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="createRange"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
start-placeholder="开始"
end-placeholder="结束"
style="width: 360px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="mt_10">
<div class="order-tab">
<el-tabs v-model="currentStatus" @tab-click="onStatusTabClick">
<el-tab-pane
v-for="item in statusTabs"
:key="item.value"
:label="item.title"
:name="item.value"
/>
</el-tabs>
</div>
<div class="operation padding-row">
<el-button type="primary" @click="goCreate">新建采购单</el-button>
</div>
<el-table v-loading="loading" :data="data" border class="mt_10" style="width: 100%">
<el-table-column prop="orderSn" label="采购单号" width="220" show-overflow-tooltip />
<el-table-column prop="totalQuantity" label="采购数量" width="90" align="center" />
<el-table-column label="采购金额" width="110" align="right">
<template #default="{ row }">{{ formatMoney(row.totalAmount) }}</template>
</el-table-column>
<el-table-column prop="makerName" label="制单人" width="120" show-overflow-tooltip />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="procurementStatusTag(row.status)">{{ procurementStatusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column prop="auditTime" label="审核时间" width="170" />
<el-table-column prop="auditorName" label="审核人" width="120" show-overflow-tooltip />
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="{ row }">
<a class="link-text" @click="goDetail(row)">查看</a>
<template v-if="row.status === 'DRAFT'">
<span class="op-split">|</span>
<a class="link-text" @click="handleSubmit(row)">提交</a>
</template>
<template v-if="row.status === 'SUBMITTED'">
<span class="op-split">|</span>
<a class="link-text" @click="handleAudit(row, true)">通过</a>
<span class="op-split">|</span>
<a class="link-text" @click="handleAudit(row, false)">驳回</a>
</template>
<template v-if="['DRAFT', 'SUBMITTED', 'PENDING_INBOUND', 'PARTIAL_INBOUND', 'REJECTED'].includes(row.status)">
<span class="op-split">|</span>
<a class="link-text" @click="handleClose(row)">关闭</a>
</template>
<template v-if="['PENDING_INBOUND', 'PARTIAL_INBOUND'].includes(row.status)">
<span class="op-split">|</span>
<a class="link-text" @click="goInbound(row)">入库</a>
</template>
</template>
</el-table-column>
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="getDataList"
@size-change="getDataList"
/>
</div>
</el-card>
</div>
</template>
<script>
import {
getProcurementOrderPage,
getProcurementOrderStatusCount,
submitProcurementOrder,
auditProcurementOrder,
closeProcurementOrder,
} from "@/api/procurement";
import {
PROCUREMENT_STATUS,
procurementStatusText,
procurementStatusTag,
formatMoney,
} from "../constants";
export default {
name: "procurement-purchase-order-list",
data() {
return {
loading: false,
data: [],
total: 0,
createRange: [],
currentStatus: "ALL",
statusCountData: {},
searchForm: {
pageNumber: 1,
pageSize: 10,
orderSn: "",
goodsName: "",
status: "",
sort: "createTime",
order: "desc",
},
};
},
computed: {
statusTabs() {
const tabs = [{ title: "全部", value: "ALL" }];
Object.keys(PROCUREMENT_STATUS).forEach((key) => {
const count = this.statusCountData[key];
const label = PROCUREMENT_STATUS[key];
tabs.push({
title: count ? `${label}${count}` : label,
value: key,
});
});
return tabs;
},
},
mounted() {
this.init();
},

View File

@@ -0,0 +1,350 @@
<template>
<div class="search">
<el-card>
<template #header>
<div class="card-header">
<span>{{ pageTitle }}</span>
<el-button @click="goBack">返回列表</el-button>
</div>
</template>
<el-form :model="form" label-width="100px">
<el-form-item label="出入库原因" required>
<el-select
v-model="form.stockReasonId"
:disabled="readonly"
placeholder="请选择出入库原因"
style="width: 300px"
>
<el-option
v-for="item in reasonOptions"
:key="item.id"
:label="item.reason"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="form.remark"
type="textarea"
:rows="2"
:disabled="readonly"
placeholder="备注信息"
style="max-width: 600px"
/>
</el-form-item>
</el-form>
<div v-if="!readonly" class="operation padding-row">
<el-button type="primary" @click="openSkuPicker">添加商品</el-button>
</div>
<el-table :data="items" border class="mt_10 item-table" style="width: 100%">
<el-table-column prop="goodsName" label="商品名称" width="240" show-overflow-tooltip />
<el-table-column prop="skuId" label="SKU ID" class-name="sku-id-col" :width="readonly ? 200 : 170">
<template #default="{ row }">{{ row.skuId }}</template>
</el-table-column>
<el-table-column label="零售价" width="110" align="center">
<template #default="{ row }">
<el-input
v-if="!readonly"
v-model="row.retailPrice"
class="cell-input"
@blur="onDecimalBlur(row, 'retailPrice')"
/>
<span v-else>{{ formatMoney(row.retailPrice) }}</span>
</template>
</el-table-column>
<el-table-column label="采购数量" width="100" align="center">
<template #default="{ row }">
<el-input
v-if="!readonly"
v-model="row.quantity"
class="cell-input"
@blur="onQuantityBlur(row)"
/>
<span v-else>{{ row.quantity }}</span>
</template>
</el-table-column>
<el-table-column label="税率(%)" width="90" align="center">
<template #default="{ row }">
<el-input
v-if="!readonly"
v-model="row.taxRate"
class="cell-input"
@blur="onTaxRateBlur(row)"
/>
<span v-else>{{ row.taxRate }}%</span>
</template>
</el-table-column>
<el-table-column label="含税单价" width="110" align="center">
<template #default="{ row }">
<el-input
v-if="!readonly"
v-model="row.unitPriceWithTax"
class="cell-input"
@blur="onDecimalBlur(row, 'unitPriceWithTax')"
/>
<span v-else>{{ formatMoney(row.unitPriceWithTax) }}</span>
</template>
</el-table-column>
<el-table-column label="含税小计" width="100" align="right">
<template #default="{ row }">{{ formatMoney(calcSubtotal(row)) }}</template>
</el-table-column>
<el-table-column v-if="orderDetail.id" label="已入库" width="80" align="center">
<template #default="{ row }">{{ row.receivedQuantity || 0 }}</template>
</el-table-column>
<el-table-column v-if="!readonly" label="操作" width="70" align="center" fixed="right">
<template #default="{ $index }">
<a class="link-text" @click="removeItem($index)">删除</a>
</template>
</el-table-column>
</el-table>
<div class="summary mt_10">
<span>合计数量<b>{{ totalQuantity }}</b></span>
<span class="ml_20">合计金额<b>{{ formatMoney(totalAmount) }}</b></span>
<span v-if="orderDetail.status" class="ml_20">
状态<el-tag :type="procurementStatusTag(orderDetail.status)">{{ procurementStatusText(orderDetail.status) }}</el-tag>
</span>
<span v-if="orderDetail.auditorName" class="ml_20">审核人<b>{{ orderDetail.auditorName }}</b></span>
</div>
<div v-if="!readonly" class="footer-actions mt_20">
<el-button type="primary" :loading="submitLoading" @click="handleSave(false)">保存草稿</el-button>
<el-button type="success" :loading="submitLoading" @click="handleSave(true)">保存并提交</el-button>
</div>
<div v-if="readonly && orderDetail.status === 'SUBMITTED'" class="footer-actions mt_20">
<el-button type="primary" @click="handleAudit(true)">审核通过</el-button>
<el-button type="danger" @click="handleAudit(false)">审核驳回</el-button>
</div>
</el-card>
<liliDialog ref="liliDialog" @selectedGoodsData="onGoodsSelected" />
</div>
</template>
<script>
import liliDialog from "@/views/lili-dialog";
import {
createProcurementOrder,
getProcurementOrderDetail,
submitProcurementOrder,
auditProcurementOrder,
getStockReasonList,
} from "@/api/procurement";
import {
procurementStatusText,
procurementStatusTag,
formatMoney,
} from "../constants";
export default {
name: "procurement-purchase-order-operation",
components: { liliDialog },
data() {
return {
form: { remark: "", stockReasonId: "" },
items: [],
orderDetail: {},
reasonOptions: [],
submitLoading: false,
readonly: false,
};
},
computed: {
pageTitle() {
if (this.readonly) return "采购单详情";
return this.$route.query.id ? "编辑采购单" : "新建采购单";
},
totalQuantity() {
return this.items.reduce((sum, item) => sum + (Number(item.quantity) || 0), 0);
},
totalAmount() {
return this.items.reduce((sum, item) => sum + this.calcSubtotal(item), 0);
},
},
mounted() {
this.loadReasons();
if (this.$route.query.id) {
this.readonly = true;
this.loadDetail(this.$route.query.id);
}
},
methods: {
procurementStatusText,
procurementStatusTag,
formatMoney,
loadReasons() {
getStockReasonList({ category: "INBOUND", pageNumber: 1, pageSize: 100 }).then((res) => {
if (res.success) {
this.reasonOptions = res.result.records || [];
}
});
},
calcSubtotal(row) {
return (Number(row.unitPriceWithTax) || 0) * (Number(row.quantity) || 0);
},
recalcRow() {},
onQuantityBlur(row) {
const val = parseInt(row.quantity, 10);
row.quantity = Number.isNaN(val) || val < 1 ? 1 : val;
this.recalcRow(row);
},
onTaxRateBlur(row) {
const val = parseInt(row.taxRate, 10);
if (Number.isNaN(val) || val < 0) {
row.taxRate = 0;
} else if (val > 100) {
row.taxRate = 100;
} else {
row.taxRate = val;
}
this.recalcRow(row);
},
onDecimalBlur(row, field) {
const val = parseFloat(row[field]);
row[field] = Number.isNaN(val) || val < 0 ? 0 : Math.round(val * 100) / 100;
this.recalcRow(row);
},
loadDetail(id) {
getProcurementOrderDetail(id).then((res) => {
if (res.success && res.result) {
this.orderDetail = res.result;
this.form.remark = res.result.remark || "";
this.form.stockReasonId = res.result.stockReasonId || "";
this.items = (res.result.items || []).map((item) => ({ ...item }));
}
});
},
openSkuPicker() {
this.$refs.liliDialog.goodsData = this.items.map((item) => ({
id: item.skuId,
goodsId: item.goodsId,
goodsName: item.goodsName,
price: item.retailPrice,
}));
this.$refs.liliDialog.open("goods");
},
onGoodsSelected(selected) {
const map = new Map(this.items.map((item) => [item.skuId, item]));
this.items = selected.map((sku) => {
const existed = map.get(sku.id);
if (existed) return existed;
return {
goodsId: sku.goodsId,
skuId: sku.id,
goodsName: sku.goodsName,
retailPrice: sku.price || 0,
quantity: 1,
taxRate: 0,
unitPriceWithTax: sku.cost || sku.price || 0,
};
});
},
removeItem(index) {
this.items.splice(index, 1);
},
validateItems() {
if (!this.form.stockReasonId) {
this.$Message.warning("请选择出入库原因");
return false;
}
if (!this.items.length) {
this.$Message.warning("请至少添加一个商品");
return false;
}
for (const item of this.items) {
if (!item.quantity || item.quantity < 1) {
this.$Message.warning("采购数量必须大于0");
return false;
}
if (item.unitPriceWithTax == null || item.unitPriceWithTax < 0) {
this.$Message.warning("请填写含税单价");
return false;
}
}
return true;
},
handleSave(andSubmit) {
if (!this.validateItems()) return;
this.submitLoading = true;
const payload = {
remark: this.form.remark,
stockReasonId: this.form.stockReasonId,
items: this.items.map((item) => ({
goodsId: item.goodsId,
skuId: item.skuId,
goodsName: item.goodsName,
retailPrice: item.retailPrice,
quantity: item.quantity,
taxRate: item.taxRate || 0,
unitPriceWithTax: item.unitPriceWithTax,
})),
};
createProcurementOrder(payload)
.then((res) => {
if (!res.success) return;
const orderId = res.result.id;
if (andSubmit) {
return submitProcurementOrder(orderId).then((subRes) => {
if (subRes.success) {
this.$Message.success("保存并提交成功");
this.goBack();
}
});
}
this.$Message.success("保存成功");
this.goBack();
})
.finally(() => {
this.submitLoading = false;
});
},
handleAudit(pass) {
auditProcurementOrder(this.orderDetail.id, { pass, remark: "" }).then((res) => {
if (res.success) {
this.$Message.success("操作成功");
this.goBack();
}
});
},
goBack() {
this.$filters.customRouterPush({ name: "procurementPurchaseOrderList" });
},
},
};
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.summary {
padding: 12px 0;
font-size: 14px;
}
.ml_20 {
margin-left: 20px;
}
.footer-actions {
display: flex;
gap: 12px;
}
.item-table :deep(.cell-input) {
width: 100%;
}
.item-table :deep(.cell-input .el-input__wrapper) {
padding-left: 8px;
padding-right: 8px;
}
.item-table :deep(.sku-id-col .cell) {
white-space: normal;
word-break: break-all;
line-height: 1.4;
}
</style>