|
@@ -0,0 +1,615 @@
|
|
|
|
|
+import dayjs from "dayjs";
|
|
|
|
|
+import type { PaginationProps } from "@pureadmin/table";
|
|
|
|
|
+import { deviceDetection } from "@pureadmin/utils";
|
|
|
|
|
+import { message } from "@/utils/message";
|
|
|
|
|
+import { addDialog } from "@/components/ReDialog";
|
|
|
|
|
+import {
|
|
|
|
|
+ getOrderList,
|
|
|
|
|
+ getOrderDetail,
|
|
|
|
|
+ getOrderItems,
|
|
|
|
|
+ createOrder,
|
|
|
|
|
+ deleteOrder,
|
|
|
|
|
+ submitOrder,
|
|
|
|
|
+ syncToErp,
|
|
|
|
|
+ completeOrder,
|
|
|
|
|
+ cancelOrder,
|
|
|
|
|
+ getReplenishmentSuggestions
|
|
|
|
|
+} from "@/api/replenishmentOrder";
|
|
|
|
|
+import { getDeviceList } from "@/api/device";
|
|
|
|
|
+import { reactive, ref, onMounted } from "vue";
|
|
|
|
|
+import {
|
|
|
|
|
+ ElMessageBox,
|
|
|
|
|
+ ElTag,
|
|
|
|
|
+ ElInput,
|
|
|
|
|
+ ElInputNumber,
|
|
|
|
|
+ ElDatePicker,
|
|
|
|
|
+ ElDescriptions,
|
|
|
|
|
+ ElDescriptionsItem,
|
|
|
|
|
+ ElTable,
|
|
|
|
|
+ ElTableColumn,
|
|
|
|
|
+ ElButton,
|
|
|
|
|
+ ElDivider,
|
|
|
|
|
+ ElCard
|
|
|
|
|
+} from "element-plus";
|
|
|
|
|
+import type {
|
|
|
|
|
+ ReplenishmentSearchForm,
|
|
|
|
|
+ ReplenishmentOrderRow,
|
|
|
|
|
+ ReplenishmentSuggestion,
|
|
|
|
|
+ SuggestionItem
|
|
|
|
|
+} from "./types";
|
|
|
|
|
+
|
|
|
|
|
+export function useReplenishment() {
|
|
|
|
|
+ const form = reactive<ReplenishmentSearchForm>({
|
|
|
|
|
+ orderNo: "",
|
|
|
|
|
+ deviceId: "",
|
|
|
|
|
+ status: undefined,
|
|
|
|
|
+ timeRange: []
|
|
|
|
|
+ });
|
|
|
|
|
+ const formRef = ref();
|
|
|
|
|
+ const dataList = ref<ReplenishmentOrderRow[]>([]);
|
|
|
|
|
+ const loading = ref(true);
|
|
|
|
|
+ const deviceOptions = ref<any[]>([]);
|
|
|
|
|
+ const pagination = reactive<PaginationProps>({
|
|
|
|
|
+ total: 0,
|
|
|
|
|
+ pageSize: 10,
|
|
|
|
|
+ currentPage: 1,
|
|
|
|
|
+ background: true
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const statusMap: Record<number, { text: string; type: string }> = {
|
|
|
|
|
+ 0: { text: "草稿", type: "info" },
|
|
|
|
|
+ 1: { text: "已提交", type: "warning" },
|
|
|
|
|
+ 2: { text: "已同步ERP", type: "primary" },
|
|
|
|
|
+ 3: { text: "已完成", type: "success" },
|
|
|
|
|
+ 4: { text: "已取消", type: "danger" }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const columns: TableColumnList = [
|
|
|
|
|
+ { label: "补货单号", prop: "orderNo", minWidth: 180 },
|
|
|
|
|
+ { label: "门店名称", prop: "shopName", minWidth: 130 },
|
|
|
|
|
+ { label: "设备ID", prop: "deviceId", minWidth: 130 },
|
|
|
|
|
+ { label: "设备名称", prop: "deviceName", minWidth: 130 },
|
|
|
|
|
+ { label: "补货数量", prop: "totalQuantity", width: 100 },
|
|
|
|
|
+ {
|
|
|
|
|
+ label: "补货金额",
|
|
|
|
|
+ prop: "totalAmount",
|
|
|
|
|
+ width: 110,
|
|
|
|
|
+ formatter: ({ totalAmount }: any) =>
|
|
|
|
|
+ totalAmount != null ? `¥${Number(totalAmount).toFixed(2)}` : "-"
|
|
|
|
|
+ },
|
|
|
|
|
+ { label: "供应商", prop: "supplierName", minWidth: 120 },
|
|
|
|
|
+ {
|
|
|
|
|
+ label: "状态",
|
|
|
|
|
+ prop: "status",
|
|
|
|
|
+ width: 100,
|
|
|
|
|
+ cellRenderer: ({ row }: any) => {
|
|
|
|
|
+ const s = statusMap[row.status];
|
|
|
|
|
+ return s ? <ElTag type={s.type as any}>{s.text}</ElTag> : "-";
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ { label: "创建人", prop: "creatorName", width: 100 },
|
|
|
|
|
+ { label: "明细数", prop: "itemCount", width: 80 },
|
|
|
|
|
+ {
|
|
|
|
|
+ label: "创建时间",
|
|
|
|
|
+ prop: "createTime",
|
|
|
|
|
+ minWidth: 160,
|
|
|
|
|
+ formatter: ({ createTime }: any) =>
|
|
|
|
|
+ createTime ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss") : "-"
|
|
|
|
|
+ },
|
|
|
|
|
+ { label: "操作", fixed: "right", width: 280, slot: "operation" }
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 搜索 / 分页 ====================
|
|
|
|
|
+
|
|
|
|
|
+ async function onSearch() {
|
|
|
|
|
+ loading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const params: any = {
|
|
|
|
|
+ page: pagination.currentPage,
|
|
|
|
|
+ pageSize: pagination.pageSize
|
|
|
|
|
+ };
|
|
|
|
|
+ if (form.orderNo) params.orderNo = form.orderNo;
|
|
|
|
|
+ if (form.deviceId) params.deviceId = form.deviceId;
|
|
|
|
|
+ if (form.status !== undefined && form.status !== null && form.status !== "")
|
|
|
|
|
+ params.status = form.status;
|
|
|
|
|
+ if (form.timeRange?.length === 2) {
|
|
|
|
|
+ params.startTime = form.timeRange[0];
|
|
|
|
|
+ params.endTime = form.timeRange[1];
|
|
|
|
|
+ }
|
|
|
|
|
+ const { data } = await getOrderList(params);
|
|
|
|
|
+ if (data) {
|
|
|
|
|
+ dataList.value = (data.list || []) as ReplenishmentOrderRow[];
|
|
|
|
|
+ pagination.total = Number(data.total) || 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error("获取补货单列表失败:", e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setTimeout(() => { loading.value = false; }, 300);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function resetForm(formEl: any) {
|
|
|
|
|
+ if (!formEl) return;
|
|
|
|
|
+ formEl.resetFields();
|
|
|
|
|
+ (form as any).timeRange = [];
|
|
|
|
|
+ pagination.currentPage = 1;
|
|
|
|
|
+ onSearch();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function handleSizeChange(val: number) {
|
|
|
|
|
+ pagination.pageSize = val;
|
|
|
|
|
+ onSearch();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function handleCurrentChange(val: number) {
|
|
|
|
|
+ pagination.currentPage = val;
|
|
|
|
|
+ onSearch();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function fetchDeviceOptions() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { data } = await getDeviceList({ page: 1, pageSize: 1000 });
|
|
|
|
|
+ deviceOptions.value = data?.list || [];
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error("获取设备列表失败:", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 创建补货单弹窗 ====================
|
|
|
|
|
+
|
|
|
|
|
+ function handleCreate() {
|
|
|
|
|
+ const selectedDevices = ref<any[]>([]);
|
|
|
|
|
+ const suggestions = ref<ReplenishmentSuggestion[]>([]);
|
|
|
|
|
+ const suggestionsLoading = ref(false);
|
|
|
|
|
+ const commonForm = reactive({
|
|
|
|
|
+ supplierName: "",
|
|
|
|
|
+ warehouseName: "",
|
|
|
|
|
+ expectedArrivalTime: ""
|
|
|
|
|
+ });
|
|
|
|
|
+ const innerDeviceOptions = ref<any[]>([]);
|
|
|
|
|
+
|
|
|
|
|
+ async function loadDevices() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { data } = await getDeviceList({ page: 1, pageSize: 1000 });
|
|
|
|
|
+ innerDeviceOptions.value = data?.list || [];
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error("加载设备失败:", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function handleSelectionChange(selection: any[]) {
|
|
|
|
|
+ selectedDevices.value = selection;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function fetchSuggestions() {
|
|
|
|
|
+ if (selectedDevices.value.length === 0) {
|
|
|
|
|
+ message("请先勾选设备", { type: "warning" });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ suggestionsLoading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const ids = selectedDevices.value.map((d: any) => d.deviceId as string);
|
|
|
|
|
+ const { data } = await getReplenishmentSuggestions(ids);
|
|
|
|
|
+ const list: ReplenishmentSuggestion[] = Array.isArray(data) ? data : [];
|
|
|
|
|
+ if (list.length === 0) {
|
|
|
|
|
+ message("所选设备当前均无需补货", { type: "info" });
|
|
|
|
|
+ }
|
|
|
|
|
+ suggestions.value = list;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error("获取补货建议失败:", e);
|
|
|
|
|
+ message("获取补货建议失败", { type: "error" });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ suggestionsLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function doSubmit(done: () => void) {
|
|
|
|
|
+ const validSuggestions = suggestions.value.filter(
|
|
|
|
|
+ s => s.items.some(i => i.suggestedQuantity > 0)
|
|
|
|
|
+ );
|
|
|
|
|
+ if (validSuggestions.length === 0) {
|
|
|
|
|
+ message("没有有效的补货项,请检查补货数量", { type: "warning" });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const payloads = validSuggestions.map(s => ({
|
|
|
|
|
+ deviceId: s.deviceId,
|
|
|
|
|
+ shopId: s.shopId ? Number(s.shopId) : undefined,
|
|
|
|
|
+ supplierName: commonForm.supplierName || undefined,
|
|
|
|
|
+ warehouseName: commonForm.warehouseName || undefined,
|
|
|
|
|
+ expectedArrivalTime: commonForm.expectedArrivalTime
|
|
|
|
|
+ ? dayjs(commonForm.expectedArrivalTime).format("YYYY-MM-DD HH:mm:ss")
|
|
|
|
|
+ : undefined,
|
|
|
|
|
+ items: s.items
|
|
|
|
|
+ .filter(i => i.suggestedQuantity > 0)
|
|
|
|
|
+ .map(i => ({
|
|
|
|
|
+ productId: Number(i.productId),
|
|
|
|
|
+ productCode: i.productCode,
|
|
|
|
|
+ productName: i.productName,
|
|
|
|
|
+ plannedQuantity: i.suggestedQuantity,
|
|
|
|
|
+ unitPrice: i.unitPrice != null ? i.unitPrice : undefined,
|
|
|
|
|
+ shelfNum: i.shelfNum,
|
|
|
|
|
+ position: i.position
|
|
|
|
|
+ }))
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ const results = await Promise.allSettled(
|
|
|
|
|
+ payloads.map(p => createOrder(p))
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ const successCount = results.filter(r => r.status === "fulfilled").length;
|
|
|
|
|
+ const failCount = results.filter(r => r.status === "rejected").length;
|
|
|
|
|
+
|
|
|
|
|
+ if (failCount === 0) {
|
|
|
|
|
+ message(`成功创建 ${successCount} 个补货单`, { type: "success" });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ message(`创建完成:${successCount} 成功, ${failCount} 失败`, { type: "warning" });
|
|
|
|
|
+ }
|
|
|
|
|
+ done();
|
|
|
|
|
+ onSearch();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const createContentRenderer = () => {
|
|
|
|
|
+ const hasSuggestions = suggestions.value.length > 0;
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div style="max-height: 65vh; overflow-y: auto;">
|
|
|
|
|
+ {/* ===== 第一步:设备列表 ===== */}
|
|
|
|
|
+ <div style="margin-bottom:8px;font-size:14px;font-weight:600;color:#303133;">
|
|
|
|
|
+ 第一步:勾选需要补货的设备
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <ElTable
|
|
|
|
|
+ data={innerDeviceOptions.value}
|
|
|
|
|
+ max-height={220}
|
|
|
|
|
+ onSelectionChange={handleSelectionChange}
|
|
|
|
|
+ >
|
|
|
|
|
+ <ElTableColumn type="selection" width="55" />
|
|
|
|
|
+ <ElTableColumn prop="deviceId" label="设备ID" minWidth={140} />
|
|
|
|
|
+ <ElTableColumn prop="name" label="设备名称" minWidth={140} />
|
|
|
|
|
+ <ElTableColumn prop="shopName" label="所属门店" minWidth={140} />
|
|
|
|
|
+ </ElTable>
|
|
|
|
|
+
|
|
|
|
|
+ <div style="margin-top:12px;display:flex;align-items:center;gap:12px;">
|
|
|
|
|
+ <ElButton
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ loading={suggestionsLoading.value}
|
|
|
|
|
+ onClick={fetchSuggestions}
|
|
|
|
|
+ disabled={selectedDevices.value.length === 0}
|
|
|
|
|
+ >
|
|
|
|
|
+ 补货建议
|
|
|
|
|
+ </ElButton>
|
|
|
|
|
+ <span style="color:#909399;font-size:13px;">
|
|
|
|
|
+ 已勾选 {selectedDevices.value.length} 台设备
|
|
|
|
|
+ {hasSuggestions
|
|
|
|
|
+ ? `,共 ${suggestions.value.reduce((s: number, sug) => s + sug.items.length, 0)} 项待补商品`
|
|
|
|
|
+ : ""}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* ===== 第二步:补货明细 ===== */}
|
|
|
|
|
+ {hasSuggestions && (
|
|
|
|
|
+ <div style="margin-top:24px;">
|
|
|
|
|
+ <div style="margin-bottom:12px;font-size:14px;font-weight:600;color:#303133;">
|
|
|
|
|
+ 第二步:确认补货明细,填写供应商等信息
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:16px;padding:12px;background:#fafafa;border-radius:6px;">
|
|
|
|
|
+ <div style="display:flex;align-items:center;gap:6px;">
|
|
|
|
|
+ <span style="font-size:13px;color:#606266;white-space:nowrap;">供应商</span>
|
|
|
|
|
+ <ElInput v-model={commonForm.supplierName} placeholder="选填" clearable size="small" style="width:150px;" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="display:flex;align-items:center;gap:6px;">
|
|
|
|
|
+ <span style="font-size:13px;color:#606266;white-space:nowrap;">仓库</span>
|
|
|
|
|
+ <ElInput v-model={commonForm.warehouseName} placeholder="选填" clearable size="small" style="width:150px;" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="display:flex;align-items:center;gap:6px;">
|
|
|
|
|
+ <span style="font-size:13px;color:#606266;white-space:nowrap;">预计到货</span>
|
|
|
|
|
+ <ElDatePicker v-model={commonForm.expectedArrivalTime} type="datetime" placeholder="选填"
|
|
|
|
|
+ value-format="YYYY-MM-DD HH:mm:ss" size="small" style="width:190px;" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {suggestions.value.map((sug: ReplenishmentSuggestion) => {
|
|
|
|
|
+ const deviceTotalQty = sug.items.reduce(
|
|
|
|
|
+ (sum: number, i) => sum + (i.suggestedQuantity || 0), 0
|
|
|
|
|
+ );
|
|
|
|
|
+ const deviceTotalAmt = sug.items.reduce(
|
|
|
|
|
+ (sum: number, i) => sum + (i.suggestedQuantity || 0) * (i.unitPrice || 0), 0
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ const itemRows = sug.items.map((item: SuggestionItem, idx: number) => (
|
|
|
|
|
+ <tr key={idx}>
|
|
|
|
|
+ <td style="padding:6px 10px;border:1px solid #ebeef5;font-size:13px;">{item.productCode}</td>
|
|
|
|
|
+ <td style="padding:6px 10px;border:1px solid #ebeef5;font-size:13px;">{item.productName || "-"}</td>
|
|
|
|
|
+ <td style="padding:6px 10px;border:1px solid #ebeef5;text-align:center;font-size:13px;">{item.templateStock}</td>
|
|
|
|
|
+ <td style="padding:6px 10px;border:1px solid #ebeef5;text-align:center;font-size:13px;">
|
|
|
|
|
+ <span style={`font-weight:500;color:${item.currentStock <= 0 ? '#f56c6c' : '#67c23a'}`}>{item.currentStock}</span>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td style="padding:4px 6px;border:1px solid #ebeef5;">
|
|
|
|
|
+ <ElInputNumber v-model={item.suggestedQuantity} min={1} max={9999} size="small" controls-position="right" style="width:100px;" />
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td style="padding:4px 6px;border:1px solid #ebeef5;">
|
|
|
|
|
+ <ElInput v-model={item.unitPrice} placeholder="单价" size="small" style="width:90px;" />
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td style="padding:6px 10px;border:1px solid #ebeef5;text-align:right;font-size:13px;font-weight:500;">
|
|
|
|
|
+ ¥{((item.suggestedQuantity || 0) * (item.unitPrice || 0)).toFixed(2)}
|
|
|
|
|
+ </td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ ));
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <ElCard key={sug.deviceId} style="margin-bottom:12px;" shadow="hover">
|
|
|
|
|
+ {{
|
|
|
|
|
+ header: () => (
|
|
|
|
|
+ <div style="display:flex;justify-content:space-between;align-items:center;">
|
|
|
|
|
+ <span style="font-size:14px;font-weight:600;">
|
|
|
|
|
+ {sug.deviceId}{sug.deviceName ? ` (${sug.deviceName})` : ""}
|
|
|
|
|
+ <span style="margin:0 8px;color:#dcdfe6;">|</span>
|
|
|
|
|
+ {sug.shopName || "未绑定门店"}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span style="color:#409eff;font-size:13px;">{deviceTotalQty} 件 / ¥{deviceTotalAmt.toFixed(2)}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ),
|
|
|
|
|
+ default: () => (
|
|
|
|
|
+ <table style="width:100%;border-collapse:collapse;">
|
|
|
|
|
+ <thead>
|
|
|
|
|
+ <tr style="background:#f5f7fa;">
|
|
|
|
|
+ <th style="padding:8px 10px;border:1px solid #ebeef5;font-size:13px;font-weight:500;color:#606266;">商品编码</th>
|
|
|
|
|
+ <th style="padding:8px 10px;border:1px solid #ebeef5;font-size:13px;font-weight:500;color:#606266;">商品名称</th>
|
|
|
|
|
+ <th style="padding:8px 10px;border:1px solid #ebeef5;font-size:13px;font-weight:500;color:#606266;">标准库存</th>
|
|
|
|
|
+ <th style="padding:8px 10px;border:1px solid #ebeef5;font-size:13px;font-weight:500;color:#606266;">当前库存</th>
|
|
|
|
|
+ <th style="padding:8px 10px;border:1px solid #ebeef5;font-size:13px;font-weight:500;color:#606266;">补货数量</th>
|
|
|
|
|
+ <th style="padding:8px 10px;border:1px solid #ebeef5;font-size:13px;font-weight:500;color:#606266;">单价</th>
|
|
|
|
|
+ <th style="padding:8px 10px;border:1px solid #ebeef5;font-size:13px;font-weight:500;color:#606266;">小计</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody>{itemRows}</tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ )
|
|
|
|
|
+ }}
|
|
|
|
|
+ </ElCard>
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 提示 */}
|
|
|
|
|
+ <div style="margin-top:16px;padding:10px 16px;background:#ecf5ff;border-radius:6px;color:#409eff;font-size:13px;">
|
|
|
|
|
+ 确认补货明细无误后,点击「确认」按钮即可创建补货单
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ loadDevices();
|
|
|
|
|
+ addDialog({
|
|
|
|
|
+ title: "创建补货单",
|
|
|
|
|
+ width: "80%",
|
|
|
|
|
+ draggable: true,
|
|
|
|
|
+ closeOnClickModal: false,
|
|
|
|
|
+ fullscreen: deviceDetection(),
|
|
|
|
|
+ contentRenderer: createContentRenderer,
|
|
|
|
|
+ beforeSure: async (done: () => void) => {
|
|
|
|
|
+ // 确保用户已获取补货建议
|
|
|
|
|
+ if (suggestions.value.length === 0) {
|
|
|
|
|
+ message("请先勾选设备并点击「补货建议」生成补货明细", { type: "warning" });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ await doSubmit(done);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 查看详情弹窗 ====================
|
|
|
|
|
+
|
|
|
|
|
+ function handleViewDetail(row: ReplenishmentOrderRow) {
|
|
|
|
|
+ const orderDetail = ref<any>(null);
|
|
|
|
|
+ const items = ref<any[]>([]);
|
|
|
|
|
+ const detailLoading = ref(true);
|
|
|
|
|
+
|
|
|
|
|
+ async function loadDetail() {
|
|
|
|
|
+ detailLoading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const [orderRes, itemsRes] = await Promise.all([
|
|
|
|
|
+ getOrderDetail(row.id),
|
|
|
|
|
+ getOrderItems(row.id)
|
|
|
|
|
+ ]);
|
|
|
|
|
+ orderDetail.value = orderRes.data || (orderRes as any);
|
|
|
|
|
+ items.value = Array.isArray(itemsRes.data) ? itemsRes.data : [];
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error("获取补货单详情失败:", e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ detailLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const detailRenderer = () => {
|
|
|
|
|
+ if (detailLoading.value) {
|
|
|
|
|
+ return <div style="text-align:center;padding:40px;">加载中...</div>;
|
|
|
|
|
+ }
|
|
|
|
|
+ const o = orderDetail.value || {};
|
|
|
|
|
+ const s = statusMap[o.status as number];
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div style="max-height:60vh;overflow-y:auto;">
|
|
|
|
|
+ <ElDescriptions column={2} border size="small">
|
|
|
|
|
+ <ElDescriptionsItem label="补货单号" span={2}>
|
|
|
|
|
+ {o.orderNo || "-"}
|
|
|
|
|
+ </ElDescriptionsItem>
|
|
|
|
|
+ <ElDescriptionsItem label="设备ID">{o.deviceId || "-"}</ElDescriptionsItem>
|
|
|
|
|
+ <ElDescriptionsItem label="设备名称">{o.deviceName || "-"}</ElDescriptionsItem>
|
|
|
|
|
+ <ElDescriptionsItem label="门店">{o.shopName || "-"}</ElDescriptionsItem>
|
|
|
|
|
+ <ElDescriptionsItem label="状态">
|
|
|
|
|
+ {s ? <ElTag type={s.type as any} size="small">{s.text}</ElTag> : "-"}
|
|
|
|
|
+ </ElDescriptionsItem>
|
|
|
|
|
+ <ElDescriptionsItem label="总数量">{o.totalQuantity ?? "-"}</ElDescriptionsItem>
|
|
|
|
|
+ <ElDescriptionsItem label="总金额">
|
|
|
|
|
+ {o.totalAmount != null ? `¥${Number(o.totalAmount).toFixed(2)}` : "-"}
|
|
|
|
|
+ </ElDescriptionsItem>
|
|
|
|
|
+ <ElDescriptionsItem label="供应商">
|
|
|
|
|
+ {o.supplierName || "-"}
|
|
|
|
|
+ </ElDescriptionsItem>
|
|
|
|
|
+ <ElDescriptionsItem label="仓库">
|
|
|
|
|
+ {o.warehouseName || "-"}
|
|
|
|
|
+ </ElDescriptionsItem>
|
|
|
|
|
+ <ElDescriptionsItem label="预计到货">
|
|
|
|
|
+ {o.expectedArrivalTime
|
|
|
|
|
+ ? dayjs(o.expectedArrivalTime).format("YYYY-MM-DD HH:mm:ss")
|
|
|
|
|
+ : "-"}
|
|
|
|
|
+ </ElDescriptionsItem>
|
|
|
|
|
+ <ElDescriptionsItem label="创建人">{o.creatorName || "-"}</ElDescriptionsItem>
|
|
|
|
|
+ <ElDescriptionsItem label="创建时间">
|
|
|
|
|
+ {o.createTime
|
|
|
|
|
+ ? dayjs(o.createTime).format("YYYY-MM-DD HH:mm:ss")
|
|
|
|
|
+ : "-"}
|
|
|
|
|
+ </ElDescriptionsItem>
|
|
|
|
|
+ <ElDescriptionsItem label="备注" span={2}>
|
|
|
|
|
+ {o.remark || "-"}
|
|
|
|
|
+ </ElDescriptionsItem>
|
|
|
|
|
+ </ElDescriptions>
|
|
|
|
|
+
|
|
|
|
|
+ <ElDivider contentPosition="left">补货明细</ElDivider>
|
|
|
|
|
+ <ElTable data={items.value} size="small" max-height={300}>
|
|
|
|
|
+ <ElTableColumn prop="productCode" label="商品编码" minWidth={120} />
|
|
|
|
|
+ <ElTableColumn prop="productName" label="商品名称" minWidth={130} />
|
|
|
|
|
+ <ElTableColumn prop="plannedQuantity" label="计划数量" width={90} />
|
|
|
|
|
+ <ElTableColumn prop="actualQuantity" label="实际数量" width={90} />
|
|
|
|
|
+ <ElTableColumn prop="beforeStock" label="补货前库存" width={110} />
|
|
|
|
|
+ <ElTableColumn prop="afterStock" label="补货后库存" width={110} />
|
|
|
|
|
+ <ElTableColumn
|
|
|
|
|
+ prop="unitPrice"
|
|
|
|
|
+ label="单价"
|
|
|
|
|
+ width={90}
|
|
|
|
|
+ formatter={(r: any) =>
|
|
|
|
|
+ r.unitPrice != null ? `¥${Number(r.unitPrice).toFixed(2)}` : "-"
|
|
|
|
|
+ }
|
|
|
|
|
+ />
|
|
|
|
|
+ <ElTableColumn
|
|
|
|
|
+ prop="totalPrice"
|
|
|
|
|
+ label="小计"
|
|
|
|
|
+ width={90}
|
|
|
|
|
+ formatter={(r: any) =>
|
|
|
|
|
+ r.totalPrice != null
|
|
|
|
|
+ ? `¥${Number(r.totalPrice).toFixed(2)}`
|
|
|
|
|
+ : "-"
|
|
|
|
|
+ }
|
|
|
|
|
+ />
|
|
|
|
|
+ <ElTableColumn prop="shelfNum" label="层号" width={70} />
|
|
|
|
|
+ <ElTableColumn prop="position" label="货道" width={80} />
|
|
|
|
|
+ </ElTable>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ loadDetail();
|
|
|
|
|
+ addDialog({
|
|
|
|
|
+ title: `补货单详情 - ${row.orderNo}`,
|
|
|
|
|
+ width: "70%",
|
|
|
|
|
+ draggable: true,
|
|
|
|
|
+ closeOnClickModal: false,
|
|
|
|
|
+ fullscreen: deviceDetection(),
|
|
|
|
|
+ contentRenderer: detailRenderer,
|
|
|
|
|
+ hideFooter: true
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 生命周期操作 ====================
|
|
|
|
|
+
|
|
|
|
|
+ async function handleSubmit(row: ReplenishmentOrderRow) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await ElMessageBox.confirm(`确认提交补货单 ${row.orderNo}?`, "提示", {
|
|
|
|
|
+ confirmButtonText: "确定",
|
|
|
|
|
+ cancelButtonText: "取消",
|
|
|
|
|
+ type: "warning"
|
|
|
|
|
+ });
|
|
|
|
|
+ await submitOrder(row.id);
|
|
|
|
|
+ message("提交成功", { type: "success" });
|
|
|
|
|
+ onSearch();
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // cancelled
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function handleDelete(row: ReplenishmentOrderRow) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await deleteOrder(row.id);
|
|
|
|
|
+ message("删除成功", { type: "success" });
|
|
|
|
|
+ onSearch();
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error("删除补货单失败:", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function handleSyncErp(row: ReplenishmentOrderRow) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await ElMessageBox.confirm(
|
|
|
|
|
+ `确认将补货单 ${row.orderNo} 同步到ERP?`,
|
|
|
|
|
+ "提示",
|
|
|
|
|
+ { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
|
|
|
|
|
+ );
|
|
|
|
|
+ await syncToErp(row.id);
|
|
|
|
|
+ message("同步成功", { type: "success" });
|
|
|
|
|
+ onSearch();
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // cancelled
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function handleComplete(row: ReplenishmentOrderRow) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await ElMessageBox.confirm(
|
|
|
|
|
+ `确认完成补货单 ${row.orderNo}?`,
|
|
|
|
|
+ "提示",
|
|
|
|
|
+ { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
|
|
|
|
|
+ );
|
|
|
|
|
+ await completeOrder(row.id);
|
|
|
|
|
+ message("完成", { type: "success" });
|
|
|
|
|
+ onSearch();
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // cancelled
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function handleCancel(row: ReplenishmentOrderRow) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await ElMessageBox.confirm(
|
|
|
|
|
+ `确认取消补货单 ${row.orderNo}?取消后不可恢复。`,
|
|
|
|
|
+ "提示",
|
|
|
|
|
+ { confirmButtonText: "确定", cancelButtonText: "取消", type: "error" }
|
|
|
|
|
+ );
|
|
|
|
|
+ await cancelOrder(row.id);
|
|
|
|
|
+ message("已取消", { type: "success" });
|
|
|
|
|
+ onSearch();
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // cancelled
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 初始化 ====================
|
|
|
|
|
+
|
|
|
|
|
+ onMounted(async () => {
|
|
|
|
|
+ await fetchDeviceOptions();
|
|
|
|
|
+ onSearch();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ form,
|
|
|
|
|
+ formRef,
|
|
|
|
|
+ loading,
|
|
|
|
|
+ columns,
|
|
|
|
|
+ dataList,
|
|
|
|
|
+ pagination,
|
|
|
|
|
+ deviceOptions,
|
|
|
|
|
+ statusMap,
|
|
|
|
|
+ onSearch,
|
|
|
|
|
+ resetForm,
|
|
|
|
|
+ handleSizeChange,
|
|
|
|
|
+ handleCurrentChange,
|
|
|
|
|
+ handleCreate,
|
|
|
|
|
+ handleViewDetail,
|
|
|
|
|
+ handleDelete,
|
|
|
|
|
+ handleSubmit,
|
|
|
|
|
+ handleSyncErp,
|
|
|
|
|
+ handleComplete,
|
|
|
|
|
+ handleCancel
|
|
|
|
|
+ };
|
|
|
|
|
+}
|