| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350 |
- <script setup lang="ts">
- import { ref, reactive, computed } from "vue";
- import { message } from "@/utils/message";
- import { createOrder, getReplenishmentSuggestions } from "@/api/replenishmentOrder";
- import { getDeviceList } from "@/api/device";
- import { getDeviceInventoryStats } from "@/api/inventory";
- import type { ReplenishmentOrderItem } from "../utils/types";
- const emit = defineEmits<{
- (e: "success"): void;
- }>();
- const visible = ref(false);
- const loading = ref(false);
- const suggestionsLoading = ref(false);
- // 设备列表
- const deviceList = ref<any[]>([]);
- const selectedDevices = ref<any[]>([]);
- const showLowStockOnly = ref(false);
- const lowStockDeviceIds = ref<Set<string>>(new Set());
- // 需要补货的设备(库存不足)
- const filteredDeviceList = computed(() => {
- if (!showLowStockOnly.value) return deviceList.value;
- return deviceList.value.filter((d: any) => lowStockDeviceIds.value.has(d.deviceId));
- });
- // 补货建议结果(按设备分组)。用 reactive 替代 ref,确保数组内嵌套对象的深层属性修改可靠响应
- const suggestions = reactive<any[]>([]);
- // 公共字段
- const commonForm = reactive({
- supplierName: "",
- warehouseName: "",
- expectedArrivalTime: ""
- });
- async function open() {
- resetState();
- visible.value = true;
- await loadDevices();
- }
- function resetState() {
- selectedDevices.value = [];
- suggestions.length = 0;
- suggestionsLoading.value = false;
- showLowStockOnly.value = false;
- commonForm.supplierName = "";
- commonForm.warehouseName = "";
- commonForm.expectedArrivalTime = "";
- }
- async function loadDevices() {
- try {
- const [deviceRes, statsRes] = await Promise.all([
- getDeviceList({ page: 1, pageSize: 1000 }),
- getDeviceInventoryStats({ page: 1, pageSize: 1000 })
- ]);
- deviceList.value = deviceRes.data?.list || [];
- // 从库存统计中提取需要补货的设备ID
- const stats = statsRes.data?.list || [];
- lowStockDeviceIds.value = new Set(
- stats
- .filter((s: any) => s.stock_status === "缺货" || s.stock_status === "低库存")
- .map((s: any) => s.device_id)
- );
- } catch (e) {
- console.error("加载设备列表失败:", e);
- }
- }
- function handleSelectionChange(selection: any[]) {
- selectedDevices.value = selection;
- }
- function resetToSelect() {
- suggestions.length = 0;
- selectedDevices.value = [];
- }
- 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: any[] = Array.isArray(data) ? data : [];
- if (list.length === 0) {
- message("所选设备当前均无需补货", { type: "info" });
- }
- suggestions.length = 0;
- suggestions.push(...list);
- } catch (e) {
- console.error("获取补货建议失败:", e);
- message("获取补货建议失败", { type: "error" });
- } finally {
- suggestionsLoading.value = false;
- }
- }
- async function handleSubmit() {
- const validSuggestions = suggestions.filter(
- s => s.items.some((i: any) => i.suggestedQuantity > 0)
- );
- if (validSuggestions.length === 0) {
- message("没有有效的补货项,请检查补货数量", { type: "warning" });
- return;
- }
- loading.value = true;
- try {
- const payloads = validSuggestions.map((s: any) => ({
- deviceId: s.deviceId,
- shopId: s.shopId || undefined,
- supplierName: commonForm.supplierName || undefined,
- warehouseName: commonForm.warehouseName || undefined,
- expectedArrivalTime: commonForm.expectedArrivalTime || undefined,
- remark: undefined,
- items: s.items
- .filter((i: any) => i.suggestedQuantity > 0)
- .map((i: any) => ({
- productId: 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: any) => 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" });
- }
- visible.value = false;
- emit("success");
- } finally {
- loading.value = false;
- }
- }
- function deviceTotalQty(sug: any) {
- return sug.items.reduce((sum: number, i: any) => sum + (i.suggestedQuantity || 0), 0);
- }
- function deviceTotalAmt(sug: any) {
- return sug.items.reduce((sum: number, i: any) => sum + (i.suggestedQuantity || 0) * (i.unitPrice || 0), 0);
- }
- function itemSubtotal(item: any) {
- return ((item.suggestedQuantity || 0) * (item.unitPrice || 0)).toFixed(2);
- }
- defineExpose({ open });
- </script>
- <template>
- <el-dialog
- v-model="visible"
- title="新建补货单"
- width="85%"
- top="3vh"
- :close-on-click-modal="false"
- destroy-on-close
- >
- <div style="min-height: 500px; max-height: 78vh; overflow-y: auto;">
- <!-- 第一步:设备列表 -->
- <div style="margin-bottom:8px;font-size:14px;font-weight:600;color:#303133;">
- 第一步:勾选需要补货的设备
- <el-switch
- v-model="showLowStockOnly"
- active-text="仅显示需补货"
- inactive-text="全部设备"
- size="small"
- style="margin-left:16px;"
- />
- </div>
- <el-table
- ref="deviceTableRef"
- :data="filteredDeviceList"
- max-height="420"
- @selection-change="handleSelectionChange"
- >
- <el-table-column type="selection" width="55" />
- <el-table-column prop="deviceId" label="设备ID" min-width="140" />
- <el-table-column prop="name" label="设备名称" min-width="140" />
- <el-table-column prop="shopName" label="所属门店" min-width="140" />
- </el-table>
- <div style="margin-top:12px;display:flex;align-items:center;gap:12px;">
- <el-button
- type="primary"
- :loading="suggestionsLoading"
- :disabled="selectedDevices.length === 0"
- @click="fetchSuggestions"
- >
- 补货建议
- </el-button>
- <span style="color:#909399;font-size:13px;">
- 已勾选 {{ selectedDevices.length }} 台设备
- <template v-if="suggestions.length > 0">
- ,共 {{ suggestions.reduce((s, sug) => s + sug.items.length, 0) }} 项待补商品
- </template>
- </span>
- </div>
- <!-- 第二步:补货明细 -->
- <template v-if="suggestions.length > 0">
- <div style="margin-top:24px;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>
- <el-input 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>
- <el-input 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>
- <el-date-picker
- v-model="commonForm.expectedArrivalTime"
- type="datetime"
- placeholder="选填"
- value-format="YYYY-MM-DD HH:mm:ss"
- size="small"
- style="width:190px;"
- />
- </div>
- </div>
- <!-- 按设备分组 -->
- <el-card
- v-for="sug in suggestions"
- :key="sug.deviceId"
- style="margin-bottom:12px;"
- shadow="hover"
- >
- <template #header>
- <div style="display:flex;justify-content:space-between;align-items:center;">
- <span style="font-size:14px;font-weight:600;">
- {{ sug.deviceId }}
- <template v-if="sug.deviceName">({{ sug.deviceName }})</template>
- <span style="margin:0 8px;color:#dcdfe6;">|</span>
- {{ sug.shopName || "未绑定门店" }}
- </span>
- <span style="color:#409eff;font-size:13px;">
- {{ deviceTotalQty(sug) }} 件 / ¥{{ deviceTotalAmt(sug).toFixed(2) }}
- </span>
- </div>
- </template>
- <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>
- <th style="padding:8px 10px;border:1px solid #ebeef5;font-size:13px;font-weight:500;color:#606266;">小计</th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="(item, idx) in sug.items" :key="idx">
- <td style="padding:4px;border:1px solid #ebeef5;text-align:center;width:60px;">
- <img
- v-if="item.productImage"
- :src="item.productImage"
- style="width:40px;height:40px;object-fit:cover;border-radius:4px;"
- @error="$event.target.style.display='none'"
- />
- <span v-else style="color:#ccc;font-size:20px;">-</span>
- </td>
- <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="{ fontWeight:'500', color: item.currentStock <= 0 ? '#f56c6c' : '#67c23a' }">
- {{ item.currentStock }}
- </span>
- </td>
- <td style="padding:4px 6px;border:1px solid #ebeef5;">
- <el-input-number
- 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;">
- <el-input 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;">
- ¥{{ itemSubtotal(item) }}
- </td>
- </tr>
- </tbody>
- </table>
- </el-card>
- <div style="margin-top:16px;padding:10px 16px;background:#ecf5ff;border-radius:6px;color:#409eff;font-size:13px;">
- 确认补货明细无误后,点击「确认创建」按钮即可创建补货单
- </div>
- </template>
- </div>
- <template #footer>
- <div style="display:flex;justify-content:flex-end;gap:12px;">
- <el-button @click="visible = false">取消</el-button>
- <el-button
- v-if="suggestions.length > 0"
- @click="resetToSelect"
- >
- 重新选择
- </el-button>
- <el-button
- v-if="suggestions.length > 0"
- type="primary"
- :loading="loading"
- @click="handleSubmit"
- >
- 确认创建
- </el-button>
- </div>
- </template>
- </el-dialog>
- </template>
|